From 280ccc3fd6f5f27339026cff96ea91efa42f5ed9 Mon Sep 17 00:00:00 2001 From: SatoshiQaziMuhammed Date: Sun, 18 Oct 2026 09:07:10 +0400 Subject: [PATCH] DApps: the Dijital Kurdistan Ecosystem Hub, a native screen (#24) * DApps: the Dijital Kurdistan Ecosystem Hub, a native screen Tapping "Dijital Kurdistan" in the DApps tab used to open pezkuwichain.io in the browser. It now opens the hub first, inside the DApps tab so the wallet's bottom bar stays in view; the user picks an app there and it opens in the DApp browser. - Four sections: Finance (8), Governance (9), Education (4), Social (8). Each is a card with a glowing border in its colour (green, red, amber, blue), a header band with its name and app count, and a four-column grid of apps. - Apps open the same pages the web app's mobile home links to, at app.pezkuwichain.io, or their own sites: Wallet and Validators the explorer, Exchange pex.network, Delegation wekil.pex.mom, KurdAI kurdishtts.dks.news, Games games.pex.mom, Rewshenbir rewshenbir.pezkiwi.app. Music has no page yet: it is shown dimmed and does nothing when tapped. - The entry is recognised by its URL (pezkuwichain.io root, from wallet-utils dapps_full.json), so favourites open the hub too. Back, from the arrow or the system, closes the hub before it leaves the tab. - Strings in English, Turkish and Kurmanji, worded as the web app words them; other languages fall back to English. * DApps hub: open Music at music.dks.news Every app in the hub now has an address, so the dimmed, inert state for an app without one is gone along with its string. * DApps hub: open KurdMedia at dks.news --------- Co-authored-by: Claude --- common/src/main/res/values-ku/strings.xml | 42 +++ common/src/main/res/values-tr/strings.xml | 42 +++ common/src/main/res/values/strings.xml | 42 +++ .../presentation/main/MainDAppFragment.kt | 31 +- .../presentation/main/MainDAppViewModel.kt | 19 ++ .../presentation/main/hub/DksHub.kt | 113 +++++++ .../presentation/main/hub/DksHubView.kt | 289 ++++++++++++++++++ .../main/res/layout/fragment_dapp_main.xml | 32 +- 8 files changed, 599 insertions(+), 11 deletions(-) create mode 100644 feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHub.kt create mode 100644 feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHubView.kt diff --git a/common/src/main/res/values-ku/strings.xml b/common/src/main/res/values-ku/strings.xml index 78676bf5..27b325e9 100644 --- a/common/src/main/res/values-ku/strings.xml +++ b/common/src/main/res/values-ku/strings.xml @@ -2795,4 +2795,46 @@ Navnîşana serlêder Referralê bipejirîne Referral bi serkeftî hat pejirandin + + + Kurdistana Dîjîtal + Navenda Ekosîstemê + + %d Sepan + %d Sepan + + Paş + Darayî + Rêveberî + Perwerde + Civakî + Berîk + Bank + Danûstandin + Pez-DEX + P2P / Kirîn-Firotin + B2B + Bac/Zekat + Destpêk + Serok + Meclîs + Deng bide + Pejirandox + Dadwerî + Pêşniyar + Rapirsî + Nasnama + Wekîltî + Forum + Zanîngeh + Perwerde + Belge + Lêkolîn + whatsKURD + KurdMedya + KurdAI + Lîstik + Muzîk + Rewşenbîr + Referans diff --git a/common/src/main/res/values-tr/strings.xml b/common/src/main/res/values-tr/strings.xml index 405cc64b..8db479de 100644 --- a/common/src/main/res/values-tr/strings.xml +++ b/common/src/main/res/values-tr/strings.xml @@ -2083,4 +2083,46 @@ Referral kısmına bu adresi yapıştır: Başvuranın adresi Referral Onayla Referral başarıyla onaylandı + + + Dijital Kurdistan + Ekosistem Merkezi + + %d Uygulama + %d Uygulama + + Geri + Finans + Yönetişim + Eğitim + Sosyal + Cüzdan + Banka + Borsa + Pez-DEX + P2P / Al-Sat + B2B + Bac/Zekat + Launchpad + Başkan + Meclis + Oy + Doğrulayıcılar + Adalet + Öneriler + Anketler + Kimlik + Delegasyon + Forum + Üniversite + Perwerde + Sertifikalar + Araştırma + whatsKURD + KurdMedia + KurdAI + Oyunlar + Müzik + Rewshenbir + Davet diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 10f544d0..849923c8 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -2811,4 +2811,46 @@ Applicant\'s address Approve Referral Referral approved successfully + + + Dijital Kurdistan + Ecosystem Hub + + %d App + %d Apps + + Back + Finance + Governance + Education + Social + Wallet + Bank + Exchange + Pez-DEX + P2P / Buy-Sell + B2B + Bac/Zekat + Launchpad + President + Assembly + Vote + Validators + Justice + Proposals + Polls + Identity + Delegation + Forum + University + Perwerde + Certificates + Research + whatsKURD + KurdMedia + KurdAI + Games + Music + Rewshenbir + Referral diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppFragment.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppFragment.kt index bf7b33d1..044dd128 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppFragment.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppFragment.kt @@ -1,6 +1,7 @@ package io.novafoundation.nova.feature_dapp_impl.presentation.main import android.view.View +import androidx.activity.OnBackPressedCallback import androidx.recyclerview.widget.ConcatAdapter import coil.ImageLoader import io.novafoundation.nova.common.base.BaseFragment @@ -11,6 +12,7 @@ import io.novafoundation.nova.common.presentation.LoadingState import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets import io.novafoundation.nova.common.utils.recyclerView.space.SpaceBetween import io.novafoundation.nova.common.utils.recyclerView.space.addSpaceItemDecoration +import io.novafoundation.nova.common.utils.setVisible import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter import io.novafoundation.nova.feature_banners_api.presentation.bindWithAdapter import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi @@ -21,6 +23,8 @@ import io.novafoundation.nova.feature_dapp_impl.presentation.common.DAppClickHan import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappCategoryListAdapter import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappCategoryViewHolder import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel +import io.novafoundation.nova.feature_dapp_impl.presentation.main.hub.DksHubApp +import io.novafoundation.nova.feature_dapp_impl.presentation.main.hub.DksHubView import javax.inject.Inject class MainDAppFragment : @@ -28,7 +32,8 @@ class MainDAppFragment : DAppClickHandler, DAppHeaderAdapter.Handler, DappCategoriesAdapter.Handler, - MainFavoriteDAppsAdapter.Handler { + MainFavoriteDAppsAdapter.Handler, + DksHubView.Handler { override fun createBinding() = FragmentDappMainBinding.inflate(layoutInflater) @@ -45,6 +50,13 @@ class MainDAppFragment : private val dappCategoriesListAdapter by lazy(LazyThreadSafetyMode.NONE) { DappCategoryListAdapter(this) } + // Back closes the hub before it leaves the tab. + private val closeHubOnBack = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + viewModel.closeDksHub() + } + } + override fun applyInsets(rootView: View) { binder.dappRecyclerViewCatalog.applyStatusBarInsets() } @@ -53,6 +65,10 @@ class MainDAppFragment : binder.dappRecyclerViewCatalog.adapter = ConcatAdapter(headerAdapter, bannerAdapter, favoritesAdapter, dappsShimmering, dappCategoriesListAdapter) binder.dappRecyclerViewCatalog.itemAnimator = null setupRecyclerViewSpacing() + + binder.dappDksHub.hubHandler = this + binder.dappDksHub.loadLogo(imageLoader) + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, closeHubOnBack) } override fun inject() { @@ -97,6 +113,19 @@ class MainDAppFragment : favoritesAdapter.show(it.isNotEmpty()) favoritesAdapter.setDApps(it) } + + viewModel.dksHubShown.observe { shown -> + binder.dappDksHub.setVisible(shown) + closeHubOnBack.isEnabled = shown + } + } + + override fun onHubAppClicked(app: DksHubApp) { + viewModel.dksHubAppClicked(app) + } + + override fun onHubBackClicked() { + viewModel.closeDksHub() } override fun onCategoryClicked(id: String) { diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppViewModel.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppViewModel.kt index f0fc2c60..70ca9ca7 100644 --- a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppViewModel.kt +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/MainDAppViewModel.kt @@ -21,7 +21,10 @@ import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel import io.novafoundation.nova.feature_dapp_impl.presentation.common.dappCategoryToUi import io.novafoundation.nova.feature_dapp_impl.presentation.common.mapDAppCatalogToDAppCategoryModels import io.novafoundation.nova.feature_dapp_impl.presentation.common.mapFavoriteDappToDappModel +import io.novafoundation.nova.feature_dapp_impl.presentation.main.hub.DksHub +import io.novafoundation.nova.feature_dapp_impl.presentation.main.hub.DksHubApp import io.novafoundation.nova.feature_dapp_impl.presentation.main.model.DAppCategoryState +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -76,6 +79,9 @@ class MainDAppViewModel( .withLoading() .share() + /** Whether the Dijital Kurdistan hub is open over the catalog. */ + val dksHubShown = MutableStateFlow(false) + init { syncDApps() } @@ -89,9 +95,22 @@ class MainDAppViewModel( } fun dappClicked(dapp: DappModel) { + // "Dijital Kurdistan" opens its hub first; the user picks where to go from there. + if (DksHub.isEntry(dapp.url)) { + dksHubShown.value = true + return + } router.openDAppBrowser(DAppBrowserPayload.Address(dapp.url)) } + fun dksHubAppClicked(app: DksHubApp) { + router.openDAppBrowser(DAppBrowserPayload.Address(app.url)) + } + + fun closeDksHub() { + dksHubShown.value = false + } + fun searchClicked() { router.openDappSearch() } diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHub.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHub.kt new file mode 100644 index 00000000..f14e8839 --- /dev/null +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHub.kt @@ -0,0 +1,113 @@ +package io.novafoundation.nova.feature_dapp_impl.presentation.main.hub + +import android.net.Uri +import androidx.annotation.StringRes +import io.novafoundation.nova.feature_dapp_impl.R + +/* + * The Dijital Kurdistan Ecosystem Hub: what a user sees after tapping "Dijital Kurdistan" + * in the DApps tab, before choosing where to go. Four sections, each a grid of apps. + * + * Every app opens in the DApp browser. Most are pages of the web app at app.pezkuwichain.io, + * the same routes its own mobile home links to; the rest are their own sites. + */ + +data class DksHubApp( + val id: String, + val emoji: String, + @StringRes val title: Int, + val url: String, +) + +data class DksHubSection( + val id: String, + val emoji: String, + @StringRes val title: Int, + /** The section's colour, ARGB: its border, glow and header band. */ + val color: Long, + val apps: List, +) + +object DksHub { + + /** The DApps list entry that opens the hub instead of a web page (wallet-utils dapps_full.json, "Dijital Kurdistan"). */ + private const val ENTRY_HOST = "pezkuwichain.io" + + const val LOGO_URL = "https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/icons/dapps/dapps/DijitalKurdistan.png" + + fun isEntry(url: String): Boolean { + val uri = runCatching { Uri.parse(url) }.getOrNull() ?: return false + val host = uri.host?.removePrefix("www.") ?: return false + val path = uri.path.orEmpty().trim('/') + return host == ENTRY_HOST && path.isEmpty() + } + + private const val WEB_APP = "https://app.pezkuwichain.io" + private const val EXPLORER = "https://explorer.pezkuwichain.io" + + private fun web(route: String) = WEB_APP + route + + val sections: List = listOf( + DksHubSection( + id = "finance", + emoji = "💰", + title = R.string.dks_hub_section_finance, + color = 0xFF22C55E, + apps = listOf( + DksHubApp("wallet", "👛", R.string.dks_hub_app_wallet, EXPLORER), + DksHubApp("bank", "🏦", R.string.dks_hub_app_bank, web("/finance/bank")), + DksHubApp("exchange", "💱", R.string.dks_hub_app_exchange, "https://pex.network"), + DksHubApp("dex", "🔄", R.string.dks_hub_app_dex, web("/dex")), + DksHubApp("p2p", "🤝", R.string.dks_hub_app_p2p, web("/p2p")), + DksHubApp("b2b", "🤖", R.string.dks_hub_app_b2b, web("/bereketli")), + DksHubApp("zekat", "💵", R.string.dks_hub_app_zekat, web("/finance/zekat")), + DksHubApp("launchpad", "🚀", R.string.dks_hub_app_launchpad, web("/launchpad")), + ) + ), + DksHubSection( + id = "governance", + emoji = "🏛️", + title = R.string.dks_hub_section_governance, + color = 0xFFEF4444, + apps = listOf( + DksHubApp("president", "👑", R.string.dks_hub_app_president, web("/elections")), + DksHubApp("assembly", "🏛️", R.string.dks_hub_app_assembly, web("/governance/assembly")), + DksHubApp("vote", "🗳️", R.string.dks_hub_app_vote, web("/elections")), + DksHubApp("validators", "🛡️", R.string.dks_hub_app_validators, EXPLORER), + DksHubApp("justice", "⚖️", R.string.dks_hub_app_justice, web("/governance/justice")), + DksHubApp("proposals", "📜", R.string.dks_hub_app_proposals, web("/citizens/government")), + DksHubApp("polls", "📊", R.string.dks_hub_app_polls, web("/governance/polls")), + DksHubApp("identity", "🪪", R.string.dks_hub_app_identity, web("/identity")), + DksHubApp("delegation", "🔗", R.string.dks_hub_app_delegation, "https://wekil.pex.mom"), + ) + ), + DksHubSection( + id = "education", + emoji = "📚", + title = R.string.dks_hub_section_education, + color = 0xFFF59E0B, + apps = listOf( + DksHubApp("university", "🎓", R.string.dks_hub_app_university, web("/education/university")), + DksHubApp("perwerde", "📖", R.string.dks_hub_app_perwerde, web("/education")), + DksHubApp("certificates", "🏆", R.string.dks_hub_app_certificates, web("/education/certificates")), + DksHubApp("research", "🔬", R.string.dks_hub_app_research, web("/education/research")), + ) + ), + DksHubSection( + id = "social", + emoji = "💬", + title = R.string.dks_hub_section_social, + color = 0xFF3B82F6, + apps = listOf( + DksHubApp("whatskurd", "💬", R.string.dks_hub_app_whatskurd, web("/social/whatskurd")), + DksHubApp("forum", "📰", R.string.dks_hub_app_forum, web("/forum")), + DksHubApp("kurdmedia", "📺", R.string.dks_hub_app_kurdmedia, "https://dks.news"), + DksHubApp("kurdai", "🎙️", R.string.dks_hub_app_kurdai, "https://kurdishtts.dks.news"), + DksHubApp("games", "🎮", R.string.dks_hub_app_games, "https://games.pex.mom"), + DksHubApp("music", "🎵", R.string.dks_hub_app_music, "https://music.dks.news"), + DksHubApp("rewshenbir", "🦅", R.string.dks_hub_app_rewshenbir, "https://rewshenbir.pezkiwi.app"), + DksHubApp("referral", "👥", R.string.dks_hub_app_referral, web("/dashboard")), + ) + ), + ) +} diff --git a/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHubView.kt b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHubView.kt new file mode 100644 index 00000000..d4b539c1 --- /dev/null +++ b/feature-dapp-impl/src/main/java/io/novafoundation/nova/feature_dapp_impl/presentation/main/hub/DksHubView.kt @@ -0,0 +1,289 @@ +package io.novafoundation.nova.feature_dapp_impl.presentation.main.hub + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable +import android.text.TextUtils +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import android.widget.GridLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.ColorUtils +import androidx.core.widget.NestedScrollView +import coil.ImageLoader +import coil.load +import coil.transform.CircleCropTransformation +import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets +import io.novafoundation.nova.feature_dapp_impl.R + +/* + * The hub's screen, drawn in code so the whole design sits in one place: a deep navy + * page, the title with the Dijital Kurdistan mark, and one glowing card per section, + * each a four-column grid of apps. It lives inside the DApps tab, so the wallet's own + * bottom bar stays in view. + */ +class DksHubView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, +) : FrameLayout(context, attrs) { + + interface Handler { + fun onHubAppClicked(app: DksHubApp) + fun onHubBackClicked() + } + + var hubHandler: Handler? = null + + private val content = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding(dp(16), dp(8), dp(16), dp(24)) + } + + private val logo = ImageView(context) + + init { + // Swallow touches so nothing behind the hub reacts. + isClickable = true + isFocusable = true + background = GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + intArrayOf(Color.parseColor("#0B1230"), Color.parseColor("#0A0F24"), Color.parseColor("#070B1A")) + ) + + val scroll = NestedScrollView(context).apply { + isFillViewport = true + overScrollMode = View.OVER_SCROLL_NEVER + clipToPadding = false + addView(content, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)) + } + addView(scroll, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + content.applyStatusBarInsets(consume = false) + + content.addView(header()) + DksHub.sections.forEach { content.addView(section(it)) } + } + + fun loadLogo(imageLoader: ImageLoader) { + logo.load(DksHub.LOGO_URL, imageLoader) { + transformations(CircleCropTransformation()) + crossfade(true) + } + } + + // ─── header ───────────────────────────────────────────────────────────── + + private fun header(): View { + val row = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setPadding(0, dp(4), 0, dp(18)) + } + + val back = ImageView(context).apply { + setImageResource(R.drawable.ic_arrow_back) + imageTintList = ColorStateList.valueOf(Color.parseColor("#B9C6E4")) + contentDescription = context.getString(R.string.dks_hub_back) + scaleType = ImageView.ScaleType.CENTER + background = ripple(Color.TRANSPARENT, radius = dp(20).toFloat()) + setOnClickListener { hubHandler?.onHubBackClicked() } + } + row.addView(back, LinearLayout.LayoutParams(dp(40), dp(40)).apply { marginEnd = dp(6) }) + + val titles = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL } + titles.addView( + text(R.string.dks_hub_title, 28f, "#E4EEFF", bold = true).apply { + // The title's light from the hub's mark: a soft blue glow behind the letters. + setShadowLayer(dp(10).toFloat(), 0f, 0f, Color.parseColor("#803B82F6")) + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + } + ) + titles.addView(text(R.string.dks_hub_subtitle, 15f, "#8EA1C8")) + row.addView(titles, LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)) + + // The mark, in a ring of the section colours. + val ring = FrameLayout(context).apply { + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + colors = intArrayOf( + Color.parseColor("#22C55E"), + Color.parseColor("#F59E0B"), + Color.parseColor("#EF4444"), + Color.parseColor("#3B82F6"), + Color.parseColor("#22C55E") + ) + gradientType = GradientDrawable.SWEEP_GRADIENT + } + setPadding(dp(3), dp(3), dp(3), dp(3)) + } + logo.background = GradientDrawable().apply { shape = GradientDrawable.OVAL; setColor(Color.parseColor("#141C3D")) } + ring.addView(logo, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)) + row.addView(ring, LinearLayout.LayoutParams(dp(60), dp(60))) + return row + } + + // ─── sections ─────────────────────────────────────────────────────────── + + private fun section(s: DksHubSection): View { + val color = s.color.toInt() + val card = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + background = GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + intArrayOf(ColorUtils.setAlphaComponent(color, 0x33), ColorUtils.setAlphaComponent(color, 0x14)) + ).apply { + cornerRadius = dp(20).toFloat() + setStroke(dp(2), ColorUtils.setAlphaComponent(color, 0xCC)) + } + clipToOutline = true + elevation = dp(6).toFloat() + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + outlineAmbientShadowColor = color + outlineSpotShadowColor = color + } + } + + // Header band: the section's name in capitals, and how many apps it holds. + val band = LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + setPadding(dp(16), dp(12), dp(12), dp(12)) + background = GradientDrawable( + GradientDrawable.Orientation.LEFT_RIGHT, + intArrayOf(ColorUtils.setAlphaComponent(color, 0x59), ColorUtils.setAlphaComponent(color, 0x1F)) + ).apply { + cornerRadii = floatArrayOf(dp(18).toFloat(), dp(18).toFloat(), dp(18).toFloat(), dp(18).toFloat(), 0f, 0f, 0f, 0f) + } + } + band.addView( + TextView(context).apply { + text = s.emoji + setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) + }, + LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { marginEnd = dp(8) } + ) + band.addView( + TextView(context).apply { + text = context.getString(s.title).uppercase() + setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) + setTextColor(Color.WHITE) + typeface = font(bold = true) + letterSpacing = 0.04f + }, + LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) + ) + band.addView( + TextView(context).apply { + text = resources.getQuantityString(R.plurals.dks_hub_apps_count, s.apps.size, s.apps.size) + setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) + setTextColor(Color.parseColor("#E6ECF7")) + typeface = font(bold = false) + setPadding(dp(10), dp(3), dp(10), dp(3)) + background = GradientDrawable().apply { + cornerRadius = dp(12).toFloat() + setColor(ColorUtils.setAlphaComponent(Color.BLACK, 0x40)) + setStroke(dp(1), ColorUtils.setAlphaComponent(color, 0xB3)) + } + } + ) + card.addView(band) + + // A hairline in the section colour under the band. + card.addView( + View(context).apply { setBackgroundColor(ColorUtils.setAlphaComponent(color, 0x66)) }, + LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, dp(1)) + ) + + val grid = GridLayout(context).apply { + columnCount = COLUMNS + setPadding(dp(6), dp(12), dp(6), dp(10)) + } + s.apps.forEach { grid.addView(tile(it, color), tileParams()) } + card.addView(grid) + + val wrapper = FrameLayout(context).apply { setPadding(0, 0, 0, dp(18)) } + wrapper.addView(card) + return wrapper + } + + private fun tile(app: DksHubApp, color: Int): View { + val tile = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER_HORIZONTAL + setPadding(dp(2), dp(6), dp(2), dp(8)) + background = ripple(Color.TRANSPARENT, radius = dp(14).toFloat(), rippleColor = ColorUtils.setAlphaComponent(color, 0x55)) + contentDescription = context.getString(app.title) + setOnClickListener { hubHandler?.onHubAppClicked(app) } + } + // The icon sits on a soft disc of the section's colour, lit from its centre. + val icon = TextView(context).apply { + text = app.emoji + gravity = Gravity.CENTER + setTextSize(TypedValue.COMPLEX_UNIT_SP, 30f) + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + gradientType = GradientDrawable.RADIAL_GRADIENT + gradientRadius = dp(30).toFloat() + colors = intArrayOf(ColorUtils.setAlphaComponent(color, 0x55), ColorUtils.setAlphaComponent(color, 0x00)) + } + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + } + tile.addView(icon, LinearLayout.LayoutParams(dp(58), dp(58))) + tile.addView( + TextView(context).apply { + setText(app.title) + gravity = Gravity.CENTER + setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) + setTextColor(Color.parseColor("#F1F5FF")) + typeface = font(bold = true) + maxLines = 1 + ellipsize = TextUtils.TruncateAt.END + setShadowLayer(dp(2).toFloat(), 0f, dp(1).toFloat(), Color.parseColor("#99000000")) + }, + LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { topMargin = dp(4) } + ) + return tile + } + + private fun tileParams() = GridLayout.LayoutParams( + GridLayout.spec(GridLayout.UNDEFINED), + GridLayout.spec(GridLayout.UNDEFINED, 1f) + ).apply { + width = 0 + height = LayoutParams.WRAP_CONTENT + setMargins(dp(2), dp(2), dp(2), dp(2)) + } + + // ─── helpers ──────────────────────────────────────────────────────────── + + private fun text(res: Int, sp: Float, color: String, bold: Boolean = false) = TextView(context).apply { + setText(res) + setTextSize(TypedValue.COMPLEX_UNIT_SP, sp) + setTextColor(Color.parseColor(color)) + typeface = font(bold) + } + + private fun font(bold: Boolean): Typeface? = + ResourcesCompat.getFont(context, if (bold) R.font.public_sans_bold else R.font.public_sans_regular) + + private fun ripple(fill: Int, radius: Float, rippleColor: Int = Color.parseColor("#33FFFFFF")): RippleDrawable { + val shape = GradientDrawable().apply { cornerRadius = radius; setColor(fill) } + val mask = GradientDrawable().apply { cornerRadius = radius; setColor(Color.WHITE) } + return RippleDrawable(ColorStateList.valueOf(rippleColor), shape, mask) + } + + private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() + + private companion object { + const val COLUMNS = 4 + } +} diff --git a/feature-dapp-impl/src/main/res/layout/fragment_dapp_main.xml b/feature-dapp-impl/src/main/res/layout/fragment_dapp_main.xml index 125087b6..784e2861 100644 --- a/feature-dapp-impl/src/main/res/layout/fragment_dapp_main.xml +++ b/feature-dapp-impl/src/main/res/layout/fragment_dapp_main.xml @@ -1,13 +1,25 @@ - + android:layout_height="match_parent"> + + + + + + +