mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-10-20 18:56:25 +00:00
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:
1 parent
2629cf8da9
commit
280ccc3fd6
8 files changed
+599
-11
No files matched your search
+30
-1
@@ -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) {
|
||||
|
||||
+19
@@ -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()
|
||||
}
|
||||
|
||||
+113
@@ -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")),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
+289
@@ -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>
|
||||
Reference in new issue
Block a user