diff --git a/CHANGELOG.md b/CHANGELOG.md index deaa97c923..15463a17f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. ### Fixed ## [0.75.7] +### Added +* Add views to handle and apply the remote theme automatically ### Fixed * Handle npe when loading the shopping cart data and restore from it diff --git a/core/src/main/java/io/snabble/sdk/Project.kt b/core/src/main/java/io/snabble/sdk/Project.kt index a912bca9a8..328231b2d9 100644 --- a/core/src/main/java/io/snabble/sdk/Project.kt +++ b/core/src/main/java/io/snabble/sdk/Project.kt @@ -1,7 +1,10 @@ package io.snabble.sdk +import com.google.gson.Gson import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import io.snabble.sdk.auth.SnabbleAuthorizationInterceptor import io.snabble.sdk.checkout.Checkout @@ -13,6 +16,9 @@ import io.snabble.sdk.coupons.Coupons import io.snabble.sdk.encodedcodes.EncodedCodesOptions import io.snabble.sdk.events.Events import io.snabble.sdk.googlepay.GooglePayHelper +import io.snabble.sdk.remoteTheme.AppTheme +import io.snabble.sdk.remoteTheme.DarkModeColors +import io.snabble.sdk.remoteTheme.LightModeColors import io.snabble.sdk.shoppingcart.ShoppingCart import io.snabble.sdk.shoppingcart.ShoppingCartStorage import io.snabble.sdk.utils.GsonHolder @@ -39,7 +45,10 @@ import java.util.concurrent.CopyOnWriteArrayList * A project contains configuration information and backend api urls needed for a * retailer. */ -class Project internal constructor(jsonObject: JsonObject) { +class Project internal constructor( + private val gson: Gson = GsonHolder.get(), + jsonObject: JsonObject +) { /** * The unique identifier of the Project @@ -164,7 +173,8 @@ class Project internal constructor(jsonObject: JsonObject) { // refresh encoded codes options for encoded codes that contain customer cards if (encodedCodesJsonObject != null) { - encodedCodesOptions = EncodedCodesOptions.fromJsonObject(this, encodedCodesJsonObject) + encodedCodesOptions = + EncodedCodesOptions.fromJsonObject(this, encodedCodesJsonObject) } } @@ -340,6 +350,8 @@ class Project internal constructor(jsonObject: JsonObject) { lateinit var assets: Assets private set + var appTheme: AppTheme? = null + init { parse(jsonObject) } @@ -356,11 +368,22 @@ class Project internal constructor(jsonObject: JsonObject) { brand = Snabble.brands[brandId] } + val customizationConfig: JsonElement? = jsonObject["appCustomizationConfig"] + try { + val lightModeColors: LightModeColors? = gson.fromJson(customizationConfig, LightModeColors::class.java) + val darkModeColors: DarkModeColors? = gson.fromJson(customizationConfig, DarkModeColors::class.java) + appTheme = AppTheme(lightModeColors, darkModeColors) + Logger.d("AppTheme for $id loaded: $appTheme") + } catch (e: JsonSyntaxException) { + Logger.e(e.message) + } + val urls = mutableMapOf() val links = jsonObject["links"].asJsonObject links.entrySet().forEach { urls[it.key] = Snabble.absoluteUrl(it.value.asJsonObject["href"].asString) } + this.urls = urls tokensUrl = "${urls["tokens"]}?role=retailerApp" @@ -415,7 +438,7 @@ class Project internal constructor(jsonObject: JsonObject) { paymentMethodDescriptors = jsonObject["paymentMethodDescriptors"]?.let { val typeToken = object : TypeToken?>() {}.type - val paymentMethodDescriptors = GsonHolder.get().fromJson>(it, typeToken) + val paymentMethodDescriptors = gson.fromJson>(it, typeToken) paymentMethodDescriptors.filter { desc -> PaymentMethod.fromString(desc.id) != null } @@ -428,12 +451,13 @@ class Project internal constructor(jsonObject: JsonObject) { } if (jsonObject.has("company")) { - company = GsonHolder.get().fromJson(jsonObject["company"], Company::class.java) + company = gson.fromJson(jsonObject["company"], Company::class.java) } - val codeTemplates = jsonObject["codeTemplates"]?.asJsonObject?.entrySet()?.map { (name, pattern) -> - CodeTemplate(name, pattern.asString) - }?.toMutableList() ?: mutableListOf() + val codeTemplates = + jsonObject["codeTemplates"]?.asJsonObject?.entrySet()?.map { (name, pattern) -> + CodeTemplate(name, pattern.asString) + }?.toMutableList() ?: mutableListOf() val hasDefaultTemplate = codeTemplates.any { it.name == "default" } if (!hasDefaultTemplate) { @@ -494,7 +518,7 @@ class Project internal constructor(jsonObject: JsonObject) { if (jsonObject.has("coupons")) { val couponsJsonObject = jsonObject["coupons"] val couponsType = object : TypeToken?>() {}.type - couponList = GsonHolder.get().fromJson(couponsJsonObject, couponsType) + couponList = gson.fromJson(couponsJsonObject, couponsType) } } catch (e: Exception) { Logger.e("Could not parse coupons") @@ -514,7 +538,12 @@ class Project internal constructor(jsonObject: JsonObject) { checkout = Checkout(this, shoppingCartFlow.value) - productDatabase = ProductDatabase(this, shoppingCartFlow.value, "$id.sqlite3", Snabble.config.generateSearchIndex) + productDatabase = ProductDatabase( + this, + shoppingCartFlow.value, + "$id.sqlite3", + Snabble.config.generateSearchIndex + ) events = Events(this, shoppingCartFlow.value) diff --git a/core/src/main/java/io/snabble/sdk/Snabble.kt b/core/src/main/java/io/snabble/sdk/Snabble.kt index d908edbb5f..ebd309d8dd 100644 --- a/core/src/main/java/io/snabble/sdk/Snabble.kt +++ b/core/src/main/java/io/snabble/sdk/Snabble.kt @@ -620,7 +620,7 @@ object Snabble { // if it does not exist, add it if (!updated) { try { - val project = Project(jsonProject) + val project = Project(jsonObject = jsonProject) newProjects.add(project) } catch (e: IllegalArgumentException) { Logger.d(e.message) diff --git a/core/src/main/java/io/snabble/sdk/remoteTheme/AppTheme.kt b/core/src/main/java/io/snabble/sdk/remoteTheme/AppTheme.kt new file mode 100644 index 0000000000..6279403350 --- /dev/null +++ b/core/src/main/java/io/snabble/sdk/remoteTheme/AppTheme.kt @@ -0,0 +1,22 @@ +package io.snabble.sdk.remoteTheme + +import com.google.gson.annotations.SerializedName + +data class AppTheme( + val lightModeColors: LightModeColors? = null, + val darkModeColors: DarkModeColors? = null, +) + +data class LightModeColors( + @SerializedName("colorPrimary_light") val primaryColor: String, + @SerializedName("colorOnPrimary_light") val onPrimaryColor: String, + @SerializedName("colorSecondary_light") val secondaryColor: String, + @SerializedName("colorOnSecondary_light") val onSecondaryColor: String +) + +data class DarkModeColors( + @SerializedName("colorPrimary_dark") val primaryColor: String, + @SerializedName("colorOnPrimary_dark") val onPrimaryColor: String, + @SerializedName("colorSecondary_dark") val secondaryColor: String, + @SerializedName("colorOnSecondary_dark") val onSecondaryColor: String +) diff --git a/ui-toolkit/src/main/res/layout/snabble_fragment_onboarding.xml b/ui-toolkit/src/main/res/layout/snabble_fragment_onboarding.xml index a3724c49cc..fa2076de7d 100644 --- a/ui-toolkit/src/main/res/layout/snabble_fragment_onboarding.xml +++ b/ui-toolkit/src/main/res/layout/snabble_fragment_onboarding.xml @@ -40,4 +40,4 @@ tools:text="Next" android:layout_marginHorizontal="16dp" android:layout_marginBottom="32dp" /> - \ No newline at end of file + diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index dbf71b89e8..7a539d2451 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { implementation(libs.androidx.cardview) implementation(libs.androidx.core.ktx) implementation(libs.androidx.gridlayout) + implementation(libs.androidx.lifecycleLiveData) implementation(libs.android.material) implementation(libs.androidx.recyclerview) implementation(libs.androidx.startupRuntime) @@ -102,7 +103,6 @@ dependencies { implementation(libs.rekisoftLazyWorker) implementation(libs.relex.circleindicator) implementation(libs.snabble.phoneAuth.countryCodePicker) - implementation(libs.bundles.camera) implementation(libs.bundles.navigation) diff --git a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/utils/TextFieldManager.kt b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/utils/TextFieldManager.kt index 32d402a833..ddb7b458f5 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/utils/TextFieldManager.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/cart/shoppingcart/utils/TextFieldManager.kt @@ -16,6 +16,10 @@ internal class TextFieldManager( focusManager.clearFocus() keyboardController?.hide() } + + fun showKeyboard(){ + keyboardController?.show() + } fun moveFocusToNext() { focusManager.moveFocus(FocusDirection.Next) diff --git a/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt b/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt index 67ddb2ae8d..70c2cf981e 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/payment/PaymentInputViewHelper.kt @@ -13,6 +13,7 @@ import io.snabble.sdk.ui.SnabbleUI import io.snabble.sdk.ui.payment.creditcard.datatrans.ui.DatatransFragment import io.snabble.sdk.ui.payment.creditcard.fiserv.FiservInputView import io.snabble.sdk.ui.payment.externalbilling.ExternalBillingFragment.Companion.ARG_PROJECT_ID +import io.snabble.sdk.ui.remotetheme.getPrimaryColorForProject import io.snabble.sdk.ui.utils.KeyguardUtils import io.snabble.sdk.ui.utils.UIUtils import io.snabble.sdk.utils.Logger @@ -42,7 +43,9 @@ object PaymentInputViewHelper { args.putSerializable(DatatransFragment.ARG_PAYMENT_TYPE, paymentMethod) SnabbleUI.executeAction(context, SnabbleUI.Event.SHOW_DATATRANS_INPUT, args) } + usePayone -> Payone.registerCard(activity, project, paymentMethod, Snabble.formPrefillData) + useFiserv -> { args.putString(FiservInputView.ARG_PROJECT_ID, projectId) args.putSerializable(FiservInputView.ARG_PAYMENT_TYPE, paymentMethod.name) @@ -66,11 +69,19 @@ object PaymentInputViewHelper { } } } else { - AlertDialog.Builder(context) + val alertDialog = AlertDialog.Builder(context) .setMessage(R.string.Snabble_Keyguard_requireScreenLock) .setPositiveButton(R.string.Snabble_ok, null) .setCancelable(false) - .show() + .create() + + val primaryColor: Int = context.getPrimaryColorForProject(Snabble.instance.checkedInProject.value) + + alertDialog.setOnShowListener { + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(primaryColor) + } + + alertDialog.show() } } diff --git a/ui/src/main/java/io/snabble/sdk/ui/remotetheme/RemoteThemingExtensions.kt b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/RemoteThemingExtensions.kt new file mode 100644 index 0000000000..bf86784fe1 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/RemoteThemingExtensions.kt @@ -0,0 +1,51 @@ +package io.snabble.sdk.ui.remotetheme + +import android.content.Context +import android.graphics.Color +import io.snabble.sdk.Project +import io.snabble.sdk.ui.R +import io.snabble.sdk.utils.getColorByAttribute + +fun Context.getPrimaryColorForProject(project: Project?): Int { + val lightColor = project?.appTheme?.lightModeColors?.primaryColor?.asColor() + val darkColor = project?.appTheme?.darkModeColors?.primaryColor?.asColor() + return when { + isDarkMode() -> darkColor ?: lightColor ?: getColorByAttribute(R.attr.colorPrimary) + else -> lightColor ?: getColorByAttribute(R.attr.colorPrimary) + } +} + +fun Context.getOnPrimaryColorForProject(project: Project?): Int { + val lightColor = project?.appTheme?.lightModeColors?.onPrimaryColor?.asColor() + val darkColor = project?.appTheme?.darkModeColors?.onPrimaryColor?.asColor() + return when { + isDarkMode() -> darkColor ?: lightColor ?: getColorByAttribute(R.attr.colorOnPrimary) + else -> lightColor ?: getColorByAttribute(R.attr.colorOnPrimary) + } +} + +fun Context.getSecondaryColorForProject(project: Project?): Int { + val lightColor = project?.appTheme?.lightModeColors?.secondaryColor?.asColor() + val darkColor = project?.appTheme?.darkModeColors?.secondaryColor?.asColor() + return when { + isDarkMode() -> darkColor ?: lightColor ?: getColorByAttribute(R.attr.colorSecondary) + else -> lightColor ?: getColorByAttribute(R.attr.colorSecondary) + } +} + +fun Context.getOnSecondaryColorForProject(project: Project?): Int { + val lightColor = project?.appTheme?.lightModeColors?.onSecondaryColor?.asColor() + val darkColor = project?.appTheme?.darkModeColors?.onSecondaryColor?.asColor() + return when { + isDarkMode() -> darkColor ?: lightColor ?: getColorByAttribute(R.attr.colorOnSecondary) + else -> lightColor ?: getColorByAttribute(R.attr.colorOnSecondary) + } +} + +fun String.asColor() = Color.parseColor(this) + +fun Context.isDarkMode(): Boolean { + val currentNightMode = + resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK + return currentNightMode == android.content.res.Configuration.UI_MODE_NIGHT_YES +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabblePrimaryButton.kt b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabblePrimaryButton.kt new file mode 100644 index 0000000000..22db2379ff --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabblePrimaryButton.kt @@ -0,0 +1,28 @@ +package io.snabble.sdk.ui.remotetheme + +import android.content.Context +import android.util.AttributeSet +import com.google.android.material.button.MaterialButton +import io.snabble.sdk.Snabble +import io.snabble.sdk.ui.R + +/** + * A default Materialbutton which automatically sets the remote theme colors of the + * current checked in project. + */ +class SnabblePrimaryButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.materialButtonStyle, +) : MaterialButton(context, attrs, defStyleAttr) { + + init { + setProjectAppTheme() + } + + private fun setProjectAppTheme() { + val project = Snabble.checkedInProject.value + setBackgroundColor(context.getPrimaryColorForProject(project)) + setTextColor(context.getOnPrimaryColorForProject(project)) + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabblePrimaryTextView.kt b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabblePrimaryTextView.kt new file mode 100644 index 0000000000..e051ba1a74 --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabblePrimaryTextView.kt @@ -0,0 +1,27 @@ +package io.snabble.sdk.ui.remotetheme + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.snabble.sdk.Snabble + +/** + * A default AppCompatTextView which automatically sets the primary color from the remote theme + * of the current checked in project as text color. + */ +class SnabblePrimaryTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle +) : AppCompatTextView(context, attrs, defStyleAttr) { + + + init { + setProjectAppTheme() + } + + private fun setProjectAppTheme() { + val project = Snabble.checkedInProject.value + setTextColor(context.getPrimaryColorForProject(project)) + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabbleSecondaryButton.kt b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabbleSecondaryButton.kt new file mode 100644 index 0000000000..408013461d --- /dev/null +++ b/ui/src/main/java/io/snabble/sdk/ui/remotetheme/SnabbleSecondaryButton.kt @@ -0,0 +1,29 @@ +package io.snabble.sdk.ui.remotetheme + +import android.content.Context +import android.util.AttributeSet +import com.google.android.material.button.MaterialButton +import io.snabble.sdk.Snabble +import io.snabble.sdk.ui.R + +/** + * A default Materialbutton which automatically sets the primary color from the remote theme + * of the current checked in project as text color. + * + * To use it as secondary button it is required to apply the Widget.Material3.Button.TextButton style. + */ +class SnabbleSecondaryButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.materialButtonStyle, +) : MaterialButton(context, attrs, defStyleAttr) { + + init { + setProjectAppTheme() + } + + private fun setProjectAppTheme() { + val project = Snabble.checkedInProject.value + setTextColor(context.getPrimaryColorForProject(project)) + } +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java b/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java index 05dbe3ac18..338a1f0781 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java +++ b/ui/src/main/java/io/snabble/sdk/ui/scanner/SelfScanningView.java @@ -34,18 +34,19 @@ import io.snabble.sdk.ProductDatabase; import io.snabble.sdk.Project; import io.snabble.sdk.Shop; -import io.snabble.sdk.shoppingcart.ShoppingCart; import io.snabble.sdk.Snabble; import io.snabble.sdk.ViolationNotification; import io.snabble.sdk.codes.ScannedCode; import io.snabble.sdk.coupons.Coupon; import io.snabble.sdk.coupons.CouponCode; import io.snabble.sdk.coupons.CouponType; +import io.snabble.sdk.shoppingcart.ShoppingCart; import io.snabble.sdk.shoppingcart.data.listener.ShoppingCartListener; import io.snabble.sdk.shoppingcart.data.listener.SimpleShoppingCartListener; import io.snabble.sdk.ui.R; import io.snabble.sdk.ui.SnabbleUI; import io.snabble.sdk.ui.checkout.ViolationNotificationUtils; +import io.snabble.sdk.ui.remotetheme.RemoteThemingExtensionsKt; import io.snabble.sdk.ui.telemetry.Telemetry; import io.snabble.sdk.ui.utils.DelayedProgressDialog; import io.snabble.sdk.ui.utils.I18nUtils; @@ -325,19 +326,30 @@ public void searchWithBarcode() { input.setLayoutParams(lp); input.setInputType(InputType.TYPE_CLASS_NUMBER); - new AlertDialog.Builder(getContext()) + final AlertDialog alertDialog = new AlertDialog.Builder(getContext()) .setView(input) .setTitle(R.string.Snabble_Scanner_enterBarcode) .setPositiveButton(R.string.Snabble_done, (dialog, which) -> lookupAndShowProduct(ScannedCode.parse(project, input.getText().toString()))) .setNegativeButton(R.string.Snabble_cancel, null) .setOnDismissListener(dialog -> resumeBarcodeScanner()) - .create() - .show(); + .create(); + + final int primaryColor = RemoteThemingExtensionsKt.getPrimaryColorForProject( + getContext(), + Snabble.getInstance().getCheckedInProject().getLatestValue() + ); + + alertDialog.setOnShowListener(dialog -> { + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(primaryColor); + alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(primaryColor); + }); + + alertDialog.show(); input.requestFocus(); Dispatch.mainThread(() -> { - InputMethodManager inputMethodManager = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT); }); } @@ -427,7 +439,7 @@ private void resumeBarcodeScanner() { /** * Setting this to true, makes you the controller of the camera, * with the use of startScanning and stopScanning. - * + *

* Default is false which means the view controls the camera by itself * when it attached and detaches itself of the window */ diff --git a/ui/src/main/java/io/snabble/sdk/ui/search/ProductSearchView.kt b/ui/src/main/java/io/snabble/sdk/ui/search/ProductSearchView.kt index ada906a04b..c6922239aa 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/search/ProductSearchView.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/search/ProductSearchView.kt @@ -1,29 +1,44 @@ package io.snabble.sdk.ui.search import android.content.Context -import android.os.Handler -import android.os.Looper -import android.text.Editable -import android.text.TextWatcher import android.util.AttributeSet -import android.view.KeyEvent -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout import android.widget.TextView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout import io.snabble.sdk.ui.R import io.snabble.sdk.ui.SnabbleUI +import io.snabble.sdk.ui.cart.shoppingcart.utils.rememberTextFieldManager +import io.snabble.sdk.ui.payment.creditcard.shared.widget.TextInput import io.snabble.sdk.ui.telemetry.Telemetry +import io.snabble.sdk.ui.utils.ThemeWrapper import io.snabble.sdk.ui.utils.isNotNullOrBlank -open class ProductSearchView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) { +open class ProductSearchView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { private val searchableProductAdapter: SearchableProductAdapter - private val searchBar: TextInputEditText - private val searchBarTextInputLayout: TextInputLayout + private val composeContainer: ComposeView private val addCodeAsIs: TextView private var lastSearchQuery: String? = null @@ -31,12 +46,9 @@ open class ProductSearchView @JvmOverloads constructor(context: Context, attrs: set(value) { field = value if (value) { - searchBarTextInputLayout.visibility = VISIBLE + composeContainer.visibility = VISIBLE } else { - searchBarTextInputLayout.visibility = GONE - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(searchBarTextInputLayout.windowToken, 0) - searchBarTextInputLayout.clearFocus() + composeContainer.visibility = GONE } } @@ -54,44 +66,59 @@ open class ProductSearchView @JvmOverloads constructor(context: Context, attrs: field = value searchableProductAdapter.showBarcode = value } - var inputType: Int - get() = searchBar.inputType - set(value) { - searchBar.inputType = value - } init { inflate(context, R.layout.snabble_view_search_product, this) val recyclerView: RecyclerView = findViewById(R.id.recycler_view) - searchBarTextInputLayout = findViewById(R.id.search_bar_layout) - searchBar = findViewById(R.id.search_bar) - searchBar.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - override fun afterTextChanged(s: Editable) { - if (searchBarEnabled) { - search(s.toString()) + composeContainer = findViewById(R.id.compose_container) + composeContainer.setContent { + ThemeWrapper { + val textFieldManager = rememberTextFieldManager() + val focusRequester = remember { FocusRequester() } + + var searchedCode by remember { mutableStateOf("") } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + textFieldManager.showKeyboard() } - } - }) - searchBar.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, event -> - if (actionId == EditorInfo.IME_ACTION_DONE || (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_ENTER)) { - searchBar.text?.let { text -> - showScannerWithCode(text.toString()) + Box(modifier = Modifier.padding(8.dp)) { + TextInput( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = searchedCode, + label = stringResource(id = R.string.Snabble_Scanner_enterBarcode), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + textFieldManager.clearFocusAndHideKeyboard() + showScannerWithCode(searchedCode) + } + ), + onValueChanged = { value -> + if (searchBarEnabled) { + searchedCode = value + search(value) + } + } + ) } - return@OnEditorActionListener true + } - false - }) - searchBarTextInputLayout.requestFocus() + } addCodeAsIs = findViewById(R.id.add_code_as_is) recyclerView.layoutManager = LinearLayoutManager(context) searchableProductAdapter = SearchableProductAdapter() searchableProductAdapter.showBarcode = true searchableProductAdapter.showSku = showSku searchableProductAdapter.setOnProductSelectedListener(::showScannerWithCode) - searchableProductAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + searchableProductAdapter.registerAdapterDataObserver(object : + RecyclerView.AdapterDataObserver() { override fun onChanged() { onSearchUpdated() requestLayout() @@ -112,9 +139,6 @@ open class ProductSearchView @JvmOverloads constructor(context: Context, attrs: } fun search(searchQuery: String) { - if (searchBarEnabled && searchBar.text.toString() != searchQuery) { - searchBar.setText(searchQuery) - } if (lastSearchQuery == null || lastSearchQuery != searchQuery) { lastSearchQuery = searchQuery searchableProductAdapter.search(searchQuery) @@ -130,19 +154,8 @@ open class ProductSearchView @JvmOverloads constructor(context: Context, attrs: } } - fun focusTextInputAndShowKeyboard() { - val handler = Handler(Looper.getMainLooper()) - handler.postDelayed({ - searchBar.requestFocus() - val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(searchBar, 0) - }, 100) - } - - fun focusTextInput() = searchBarTextInputLayout.requestFocus() - /** allows for overriding the default action (calling SnabbleUICallback) */ fun setOnProductSelectedListener(listener: OnProductSelectedListener) { productSelectedListener = listener } -} \ No newline at end of file +} diff --git a/ui/src/main/java/io/snabble/sdk/ui/utils/ThemeWrapper.kt b/ui/src/main/java/io/snabble/sdk/ui/utils/ThemeWrapper.kt index 9ae59363eb..7ff3b29977 100644 --- a/ui/src/main/java/io/snabble/sdk/ui/utils/ThemeWrapper.kt +++ b/ui/src/main/java/io/snabble/sdk/ui/utils/ThemeWrapper.kt @@ -1,11 +1,23 @@ package io.snabble.sdk.ui.utils +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.LayoutDirection +import androidx.lifecycle.asFlow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.themeadapter.material3.createMdc3Theme +import io.snabble.sdk.Snabble +import io.snabble.sdk.remoteTheme.AppTheme +import io.snabble.sdk.ui.remotetheme.asColor @Composable internal fun ThemeWrapper(content: @Composable () -> Unit) { @@ -13,9 +25,22 @@ internal fun ThemeWrapper(content: @Composable () -> Unit) { context = LocalContext.current, layoutDirection = LayoutDirection.Ltr ) + + var currentColorScheme by remember { mutableStateOf(colorScheme) } + + val currentTheme = Snabble.checkedInProject + .asFlow() + .collectAsStateWithLifecycle(initialValue = Snabble.checkedInProject.value) + .value + ?.appTheme + + currentColorScheme = when (currentTheme) { + null -> colorScheme + else -> colorScheme?.applyTheme(appTheme = currentTheme) + } CompositionLocalProvider { MaterialTheme( - colorScheme = colorScheme ?: MaterialTheme.colorScheme, + colorScheme = currentColorScheme ?: MaterialTheme.colorScheme, typography = typography ?: MaterialTheme.typography, shapes = shapes ?: MaterialTheme.shapes, ) { @@ -23,3 +48,28 @@ internal fun ThemeWrapper(content: @Composable () -> Unit) { } } } + +private fun String.asComposeColor(): Color = Color(asColor()) + +@Composable +private fun ColorScheme.applyTheme(appTheme: AppTheme): ColorScheme = when (isSystemInDarkTheme()) { + true -> { + copy( + primary = appTheme.darkModeColors?.primaryColor?.asComposeColor() + ?: appTheme.lightModeColors?.primaryColor?.asComposeColor() ?: primary, + onPrimary = appTheme.darkModeColors?.onPrimaryColor?.asComposeColor() + ?: appTheme.lightModeColors?.onPrimaryColor?.asComposeColor() ?: onPrimary, + secondary = appTheme.darkModeColors?.secondaryColor?.asComposeColor() + ?: appTheme.lightModeColors?.secondaryColor?.asComposeColor() ?: secondary, + onSecondary = appTheme.darkModeColors?.onSecondaryColor?.asComposeColor() + ?: appTheme.lightModeColors?.onSecondaryColor?.asComposeColor() ?: onSecondary + ) + } + + false -> copy( + primary = appTheme.lightModeColors?.primaryColor?.asComposeColor() ?: primary, + onPrimary = appTheme.lightModeColors?.onPrimaryColor?.asComposeColor() ?: onPrimary, + secondary = appTheme.lightModeColors?.secondaryColor?.asComposeColor() ?: secondary, + onSecondary = appTheme.lightModeColors?.onSecondaryColor?.asComposeColor() ?: onSecondary + ) +} diff --git a/ui/src/main/res/layout/snabble_dialog_product_confirmation.xml b/ui/src/main/res/layout/snabble_dialog_product_confirmation.xml index 54f774ba28..a86e938652 100644 --- a/ui/src/main/res/layout/snabble_dialog_product_confirmation.xml +++ b/ui/src/main/res/layout/snabble_dialog_product_confirmation.xml @@ -165,7 +165,7 @@ - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_dialog_sepa_legal_info.xml b/ui/src/main/res/layout/snabble_dialog_sepa_legal_info.xml index 924fd6bdb2..49d7b379e9 100644 --- a/ui/src/main/res/layout/snabble_dialog_sepa_legal_info.xml +++ b/ui/src/main/res/layout/snabble_dialog_sepa_legal_info.xml @@ -51,4 +51,4 @@ android:layout_gravity="bottom" android:id="@+id/button" android:text="@string/Snabble.SEPA.iAgree" /> - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_fragment_coupon_detail.xml b/ui/src/main/res/layout/snabble_fragment_coupon_detail.xml index f0691d68a8..a46c02c09f 100644 --- a/ui/src/main/res/layout/snabble_fragment_coupon_detail.xml +++ b/ui/src/main/res/layout/snabble_fragment_coupon_detail.xml @@ -72,14 +72,14 @@ android:padding="8dp" tools:text="gültig bis 31.12.2022" /> - - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_fragment_selfscanning.xml b/ui/src/main/res/layout/snabble_fragment_selfscanning.xml index a88ae29471..803004f7b1 100644 --- a/ui/src/main/res/layout/snabble_fragment_selfscanning.xml +++ b/ui/src/main/res/layout/snabble_fragment_selfscanning.xml @@ -36,11 +36,11 @@ android:textAppearance="?attr/textAppearanceBodyLarge" android:text="@string/Snabble.Scanner.Camera.allowAccess"/> - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_subject_alert_dialog.xml b/ui/src/main/res/layout/snabble_subject_alert_dialog.xml index d42bdbc833..1a1ff5bbd5 100644 --- a/ui/src/main/res/layout/snabble_subject_alert_dialog.xml +++ b/ui/src/main/res/layout/snabble_subject_alert_dialog.xml @@ -43,7 +43,7 @@ android:layout_width="match_parent" android:layout_height="24dp" /> - - - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_view_checkout_bar.xml b/ui/src/main/res/layout/snabble_view_checkout_bar.xml index d8ca368da1..0c77119f00 100644 --- a/ui/src/main/res/layout/snabble_view_checkout_bar.xml +++ b/ui/src/main/res/layout/snabble_view_checkout_bar.xml @@ -92,7 +92,7 @@ - - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_view_checkout_gatekeeper.xml b/ui/src/main/res/layout/snabble_view_checkout_gatekeeper.xml index 9ce2c24834..32fad71921 100644 --- a/ui/src/main/res/layout/snabble_view_checkout_gatekeeper.xml +++ b/ui/src/main/res/layout/snabble_view_checkout_gatekeeper.xml @@ -84,7 +84,7 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true"> - - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_view_checkout_pos.xml b/ui/src/main/res/layout/snabble_view_checkout_pos.xml index 96348ebd52..ba7129535b 100644 --- a/ui/src/main/res/layout/snabble_view_checkout_pos.xml +++ b/ui/src/main/res/layout/snabble_view_checkout_pos.xml @@ -64,7 +64,7 @@ android:layout_marginBottom="16dp" android:textAppearance="?attr/textAppearanceBodyMedium" /> - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_view_payment_status.xml b/ui/src/main/res/layout/snabble_view_payment_status.xml index 2b9f26e7c2..c03ea7b828 100644 --- a/ui/src/main/res/layout/snabble_view_payment_status.xml +++ b/ui/src/main/res/layout/snabble_view_payment_status.xml @@ -161,7 +161,7 @@ android:text="@string/Snabble.PaymentStatus.AddDebitCard.message" android:textAppearance="?attr/textAppearanceBodyLarge" /> - - - - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_view_routing_gatekeeper.xml b/ui/src/main/res/layout/snabble_view_routing_gatekeeper.xml index 9fc1fe2cd8..b8cb3b5187 100644 --- a/ui/src/main/res/layout/snabble_view_routing_gatekeeper.xml +++ b/ui/src/main/res/layout/snabble_view_routing_gatekeeper.xml @@ -86,7 +86,7 @@ android:layout_alignParentBottom="true" android:layout_centerHorizontal="true"> - - - - - - + /> - - - \ No newline at end of file + diff --git a/ui/src/main/res/layout/snabble_view_shopping_cart.xml b/ui/src/main/res/layout/snabble_view_shopping_cart.xml index c2011e751c..060fc64e23 100644 --- a/ui/src/main/res/layout/snabble_view_shopping_cart.xml +++ b/ui/src/main/res/layout/snabble_view_shopping_cart.xml @@ -65,7 +65,7 @@ android:textAppearance="?attr/textAppearanceBodyLarge" android:text="@string/Snabble.Shoppingcart.EmptyState.description" /> - -