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 <noreply@anthropic.com>
This commit is contained in:
SatoshiQaziMuhammedandClaude authored and GitHub committed 2026-10-18 09:07:10 +04:00
1 parent 2629cf8da9
commit 280ccc3fd6
8 files changed
+599 -11

No files matched your search

+42
View File
@@ -2795,4 +2795,46 @@
<string name="citizenship_approve_address_hint">Navnîşana serlêder</string>
<string name="citizenship_approve_button">Referralê bipejirîne</string>
<string name="citizenship_approve_success">Referral bi serkeftî hat pejirandin</string>
<!-- Dijital Kurdistan Ecosystem Hub (DApps tab) -->
<string name="dks_hub_title">Kurdistana Dîjîtal</string>
<string name="dks_hub_subtitle">Navenda Ekosîstemê</string>
<plurals name="dks_hub_apps_count">
<item quantity="one">%d Sepan</item>
<item quantity="other">%d Sepan</item>
</plurals>
<string name="dks_hub_back">Paş</string>
<string name="dks_hub_section_finance">Darayî</string>
<string name="dks_hub_section_governance">Rêveberî</string>
<string name="dks_hub_section_education">Perwerde</string>
<string name="dks_hub_section_social">Civakî</string>
<string name="dks_hub_app_wallet">Berîk</string>
<string name="dks_hub_app_bank">Bank</string>
<string name="dks_hub_app_exchange">Danûstandin</string>
<string name="dks_hub_app_dex">Pez-DEX</string>
<string name="dks_hub_app_p2p">P2P / Kirîn-Firotin</string>
<string name="dks_hub_app_b2b">B2B</string>
<string name="dks_hub_app_zekat">Bac/Zekat</string>
<string name="dks_hub_app_launchpad">Destpêk</string>
<string name="dks_hub_app_president">Serok</string>
<string name="dks_hub_app_assembly">Meclîs</string>
<string name="dks_hub_app_vote">Deng bide</string>
<string name="dks_hub_app_validators">Pejirandox</string>
<string name="dks_hub_app_justice">Dadwerî</string>
<string name="dks_hub_app_proposals">Pêşniyar</string>
<string name="dks_hub_app_polls">Rapirsî</string>
<string name="dks_hub_app_identity">Nasnama</string>
<string name="dks_hub_app_delegation">Wekîltî</string>
<string name="dks_hub_app_forum">Forum</string>
<string name="dks_hub_app_university">Zanîngeh</string>
<string name="dks_hub_app_perwerde">Perwerde</string>
<string name="dks_hub_app_certificates">Belge</string>
<string name="dks_hub_app_research">Lêkolîn</string>
<string name="dks_hub_app_whatskurd">whatsKURD</string>
<string name="dks_hub_app_kurdmedia">KurdMedya</string>
<string name="dks_hub_app_kurdai">KurdAI</string>
<string name="dks_hub_app_games">Lîstik</string>
<string name="dks_hub_app_music">Muzîk</string>
<string name="dks_hub_app_rewshenbir">Rewşenbîr</string>
<string name="dks_hub_app_referral">Referans</string>
</resources>
+42
View File
@@ -2083,4 +2083,46 @@ Referral kısmına bu adresi yapıştır:
<string name="citizenship_approve_address_hint">Başvuranın adresi</string>
<string name="citizenship_approve_button">Referral Onayla</string>
<string name="citizenship_approve_success">Referral başarıyla onaylandı</string>
<!-- Dijital Kurdistan Ecosystem Hub (DApps tab) -->
<string name="dks_hub_title">Dijital Kurdistan</string>
<string name="dks_hub_subtitle">Ekosistem Merkezi</string>
<plurals name="dks_hub_apps_count">
<item quantity="one">%d Uygulama</item>
<item quantity="other">%d Uygulama</item>
</plurals>
<string name="dks_hub_back">Geri</string>
<string name="dks_hub_section_finance">Finans</string>
<string name="dks_hub_section_governance">Yönetişim</string>
<string name="dks_hub_section_education">Eğitim</string>
<string name="dks_hub_section_social">Sosyal</string>
<string name="dks_hub_app_wallet">Cüzdan</string>
<string name="dks_hub_app_bank">Banka</string>
<string name="dks_hub_app_exchange">Borsa</string>
<string name="dks_hub_app_dex">Pez-DEX</string>
<string name="dks_hub_app_p2p">P2P / Al-Sat</string>
<string name="dks_hub_app_b2b">B2B</string>
<string name="dks_hub_app_zekat">Bac/Zekat</string>
<string name="dks_hub_app_launchpad">Launchpad</string>
<string name="dks_hub_app_president">Başkan</string>
<string name="dks_hub_app_assembly">Meclis</string>
<string name="dks_hub_app_vote">Oy</string>
<string name="dks_hub_app_validators">Doğrulayıcılar</string>
<string name="dks_hub_app_justice">Adalet</string>
<string name="dks_hub_app_proposals">Öneriler</string>
<string name="dks_hub_app_polls">Anketler</string>
<string name="dks_hub_app_identity">Kimlik</string>
<string name="dks_hub_app_delegation">Delegasyon</string>
<string name="dks_hub_app_forum">Forum</string>
<string name="dks_hub_app_university">Üniversite</string>
<string name="dks_hub_app_perwerde">Perwerde</string>
<string name="dks_hub_app_certificates">Sertifikalar</string>
<string name="dks_hub_app_research">Araştırma</string>
<string name="dks_hub_app_whatskurd">whatsKURD</string>
<string name="dks_hub_app_kurdmedia">KurdMedia</string>
<string name="dks_hub_app_kurdai">KurdAI</string>
<string name="dks_hub_app_games">Oyunlar</string>
<string name="dks_hub_app_music">Müzik</string>
<string name="dks_hub_app_rewshenbir">Rewshenbir</string>
<string name="dks_hub_app_referral">Davet</string>
</resources>
+42
View File
@@ -2811,4 +2811,46 @@
<string name="citizenship_approve_address_hint">Applicant\'s address</string>
<string name="citizenship_approve_button">Approve Referral</string>
<string name="citizenship_approve_success">Referral approved successfully</string>
<!-- Dijital Kurdistan Ecosystem Hub (DApps tab) -->
<string name="dks_hub_title">Dijital Kurdistan</string>
<string name="dks_hub_subtitle">Ecosystem Hub</string>
<plurals name="dks_hub_apps_count">
<item quantity="one">%d App</item>
<item quantity="other">%d Apps</item>
</plurals>
<string name="dks_hub_back">Back</string>
<string name="dks_hub_section_finance">Finance</string>
<string name="dks_hub_section_governance">Governance</string>
<string name="dks_hub_section_education">Education</string>
<string name="dks_hub_section_social">Social</string>
<string name="dks_hub_app_wallet">Wallet</string>
<string name="dks_hub_app_bank">Bank</string>
<string name="dks_hub_app_exchange">Exchange</string>
<string name="dks_hub_app_dex">Pez-DEX</string>
<string name="dks_hub_app_p2p">P2P / Buy-Sell</string>
<string name="dks_hub_app_b2b">B2B</string>
<string name="dks_hub_app_zekat">Bac/Zekat</string>
<string name="dks_hub_app_launchpad">Launchpad</string>
<string name="dks_hub_app_president">President</string>
<string name="dks_hub_app_assembly">Assembly</string>
<string name="dks_hub_app_vote">Vote</string>
<string name="dks_hub_app_validators">Validators</string>
<string name="dks_hub_app_justice">Justice</string>
<string name="dks_hub_app_proposals">Proposals</string>
<string name="dks_hub_app_polls">Polls</string>
<string name="dks_hub_app_identity">Identity</string>
<string name="dks_hub_app_delegation">Delegation</string>
<string name="dks_hub_app_forum">Forum</string>
<string name="dks_hub_app_university">University</string>
<string name="dks_hub_app_perwerde">Perwerde</string>
<string name="dks_hub_app_certificates">Certificates</string>
<string name="dks_hub_app_research">Research</string>
<string name="dks_hub_app_whatskurd">whatsKURD</string>
<string name="dks_hub_app_kurdmedia">KurdMedia</string>
<string name="dks_hub_app_kurdai">KurdAI</string>
<string name="dks_hub_app_games">Games</string>
<string name="dks_hub_app_music">Music</string>
<string name="dks_hub_app_rewshenbir">Rewshenbir</string>
<string name="dks_hub_app_referral">Referral</string>
</resources>
@@ -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) {
@@ -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()
}
@@ -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<DksHubApp>,
)
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<DksHubSection> = 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")),
)
),
)
}
@@ -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
}
}
@@ -1,13 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/dappRecyclerViewCatalog"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/drawable_background_image"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingBottom="16dp"
android:scrollbars="none"
app:layoutManager="io.novafoundation.nova.common.view.recyclerview.LinearLayoutManagerFixed" />
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dappRecyclerViewCatalog"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/drawable_background_image"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingBottom="16dp"
android:scrollbars="none"
app:layoutManager="io.novafoundation.nova.common.view.recyclerview.LinearLayoutManagerFixed" />
<!-- The Dijital Kurdistan Ecosystem Hub, over the catalog while it is open. -->
<io.novafoundation.nova.feature_dapp_impl.presentation.main.hub.DksHubView
android:id="@+id/dappDksHub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>