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">
+
+
+
+
+
+
+