diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 075e20e..4073a4b 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -143,9 +143,11 @@ kotlin {
implementation(libs.android)
implementation(libs.activity)
implementation(libs.activity.compose)
+ implementation(libs.ads)
implementation(libs.appcompat)
implementation(libs.multidex)
implementation(libs.splashscreen)
+ implementation(libs.ump)
implementation(libs.html.converter)
implementation(libs.ktor.jvm)
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 3cfe894..a613d69 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
@@ -251,5 +252,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt
index db38a89..dffd5df 100644
--- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt
+++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/App.kt
@@ -35,6 +35,7 @@ class App : MultiDexApplication(), DIAware {
.detectAll()
.permitDiskReads()
.permitDiskWrites()
+ .permitCustomSlowCalls()
.penaltyLog()
.penaltyDialog()
.build()
diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt
index e01add5..dc6b271 100644
--- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt
+++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/MainActivity.kt
@@ -24,7 +24,9 @@ import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.ktx.requestAppUpdateInfo
+import dev.datlag.aniflow.other.ConsentInfo
import dev.datlag.aniflow.other.DomainVerifier
+import dev.datlag.aniflow.other.LocalConsentInfo
import dev.datlag.aniflow.other.UpdateManager
import dev.datlag.aniflow.other.UserHelper
import dev.datlag.aniflow.ui.navigation.RootComponent
@@ -65,10 +67,13 @@ class MainActivity : AppCompatActivity() {
manager.startUpdateFlow(info, this, AppUpdateOptions.defaultOptions(type))
}
+ val consentInfo = ConsentInfo(this)
+
setContent {
CompositionLocalProvider(
LocalLifecycleOwner provides lifecycleOwner,
- LocalEdgeToEdge provides true
+ LocalEdgeToEdge provides true,
+ LocalConsentInfo provides consentInfo
) {
App(
di = di
diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.android.kt
new file mode 100644
index 0000000..57ead5b
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.android.kt
@@ -0,0 +1,59 @@
+package dev.datlag.aniflow.other
+
+import android.app.Activity
+import com.google.android.gms.ads.MobileAds
+import com.google.android.ump.ConsentInformation
+import com.google.android.ump.ConsentRequestParameters
+import com.google.android.ump.UserMessagingPlatform
+import java.util.concurrent.atomic.AtomicBoolean
+
+actual class ConsentInfo(private val activity: Activity) {
+
+ private var isMobileAdsInitialized = AtomicBoolean(false)
+
+ private val consentInformation = UserMessagingPlatform.getConsentInformation(activity)
+ private val params = ConsentRequestParameters.Builder().build()
+
+ actual val privacy: Boolean
+ get() = consentInformation.privacyOptionsRequirementStatus == ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
+
+ actual fun initialize() {
+ consentInformation.requestConsentInfoUpdate(
+ activity,
+ params,
+ {
+ // success
+ UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity) {
+ // dismiss
+ initializeMobileAds()
+ }
+ },
+ {
+ // error
+ initializeMobileAds(true)
+ }
+ )
+ }
+
+ actual fun reset() {
+ consentInformation.reset()
+ }
+
+ actual fun showPrivacyForm() {
+ UserMessagingPlatform.showPrivacyOptionsForm(activity) {
+ // dismiss
+ initializeMobileAds()
+ }
+ }
+
+ private fun initializeMobileAds(force: Boolean = false) {
+ if (consentInformation.canRequestAds() || force) {
+ if (isMobileAdsInitialized.get()) {
+ return
+ }
+
+ MobileAds.initialize(activity)
+ isMobileAdsInitialized.set(true)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.android.kt
new file mode 100644
index 0000000..972b205
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.android.kt
@@ -0,0 +1,75 @@
+package dev.datlag.aniflow.ui.custom
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import com.google.android.gms.ads.AdListener
+import com.google.android.gms.ads.AdRequest
+import com.google.android.gms.ads.AdSize
+import dev.datlag.aniflow.BuildKonfig
+import dev.datlag.aniflow.Sekret
+import dev.datlag.aniflow.other.StateSaver
+import com.google.android.gms.ads.AdView
+import com.google.android.gms.ads.LoadAdError
+import com.google.android.gms.ads.VideoOptions
+import com.google.android.gms.ads.nativead.NativeAdOptions
+import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle
+import io.github.aakira.napier.Napier
+
+@Composable
+actual fun AdView(id: String, type: AdType) {
+ val nativeAdState = rememberCustomNativeAdState(
+ adUnit = id,
+ nativeAdOptions = NativeAdOptions.Builder()
+ .setVideoOptions(
+ VideoOptions.Builder()
+ .setStartMuted(true).setClickToExpandRequested(true)
+ .build()
+ ).setRequestMultipleImages(true)
+ .build(),
+ adListener = object : AdListener() {
+ override fun onAdFailedToLoad(p0: LoadAdError) {
+ super.onAdFailedToLoad(p0)
+ Napier.e(p0.message)
+ }
+ }
+ )
+
+ val nativeAd by nativeAdState.nativeAd.collectAsStateWithLifecycle()
+
+ nativeAd?.let { NativeAdCard(it, Modifier.fillMaxWidth()) }
+}
+
+actual object Ads {
+ actual fun native(): String? {
+ return if (StateSaver.sekretLibraryLoaded) {
+ Sekret.androidAdNative(BuildKonfig.packageName)
+ } else {
+ null
+ }
+ }
+
+ actual fun banner(): String? {
+ return if (StateSaver.sekretLibraryLoaded) {
+ Sekret.androidAdBanner(BuildKonfig.packageName)
+ } else {
+ null
+ }
+ }
+}
+
+@Composable
+actual fun BannerAd(id: String, modifier: Modifier) {
+ AndroidView(
+ factory = { context ->
+ AdView(context).apply {
+ adUnitId = id
+ setAdSize(AdSize.BANNER)
+ loadAd(AdRequest.Builder().build())
+ }
+ },
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAdCard.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAdCard.kt
new file mode 100644
index 0000000..7efdb0f
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAdCard.kt
@@ -0,0 +1,200 @@
+package dev.datlag.aniflow.ui.custom
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Star
+import androidx.compose.material.icons.rounded.StarOutline
+import androidx.compose.material3.Button
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.google.android.gms.ads.nativead.NativeAd
+import dev.datlag.aniflow.common.bottomShadowBrush
+import kotlin.math.roundToInt
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun NativeAdCard(
+ nativeAd: NativeAd,
+ modifier: Modifier = Modifier
+) {
+ NativeAdViewCompose(
+ modifier = modifier
+ ) { nativeAdView ->
+ SideEffect {
+ nativeAdView.setNativeAd(nativeAd)
+ }
+
+ ElevatedCard(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ NativeAdView(
+ modifier = Modifier.size(56.dp),
+ getView = {
+ nativeAdView.iconView = it
+ }
+ ) {
+ NativeAdImage(
+ model = nativeAd.icon?.uri ?: nativeAd.icon?.drawable,
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize().clip(MaterialTheme.shapes.small)
+ )
+ }
+ nativeAd.callToAction?.ifBlank { null }?.let { action ->
+ NativeAdView(
+ getView = {
+ nativeAdView.callToActionView = it
+ }
+ ) {
+ Button(
+ onClick = {
+ nativeAdView.callToActionView?.performClick()
+ }
+ ) {
+ Text(text = action)
+ }
+ }
+ }
+ }
+ nativeAd.body?.ifBlank { null }?.let { body ->
+ NativeAdView(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
+ getView = {
+ nativeAdView.bodyView = it
+ }
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = body
+ )
+ }
+ }
+
+ Box(modifier = Modifier.fillMaxWidth()) {
+ nativeAd.mediaContent?.let { media ->
+ NativeAdMediaView(
+ modifier = Modifier
+ .defaultMinSize(
+ minWidth = 120.dp,
+ minHeight = 120.dp
+ )
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium),
+ nativeAdView = nativeAdView,
+ mediaContent = media
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .bottomShadowBrush(MaterialTheme.colorScheme.secondaryContainer)
+ .padding(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Bottom)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides MaterialTheme.colorScheme.onSecondaryContainer
+ ) {
+ nativeAd.headline?.ifBlank { null }?.let { headline ->
+ NativeAdView(
+ modifier = Modifier.fillMaxWidth(),
+ getView = {
+ nativeAdView.headlineView = it
+ }
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = headline,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ maxItemsInEachRow = 2,
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically)
+ ) {
+ nativeAd.store?.ifBlank { null }?.let { store ->
+ NativeAdView(
+ getView = {
+ nativeAdView.storeView = it
+ }
+ ) {
+ Text(
+ text = store,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ nativeAd.price?.ifBlank { null }?.let { price ->
+ NativeAdView(
+ getView = {
+ nativeAdView.priceView = it
+ }
+ ) {
+ Text(text = price)
+ }
+ }
+ nativeAd.starRating?.roundToInt()?.let { rating ->
+ NativeAdView(
+ getView = {
+ nativeAdView.starRatingView = it
+ }
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ repeat(rating) {
+ Icon(
+ imageVector = Icons.Rounded.Star,
+ contentDescription = null
+ )
+ }
+ repeat(5 - rating) {
+ Icon(
+ imageVector = Icons.Rounded.StarOutline,
+ contentDescription = null
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAds.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAds.kt
new file mode 100644
index 0000000..0a5e430
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAds.kt
@@ -0,0 +1,166 @@
+package dev.datlag.aniflow.ui.custom
+
+import android.content.Context
+import android.view.ViewGroup
+import android.widget.ImageView.ScaleType
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.DefaultAlpha
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import coil3.compose.AsyncImage
+import com.google.android.gms.ads.AdListener
+import com.google.android.gms.ads.AdLoader
+import com.google.android.gms.ads.AdRequest
+import com.google.android.gms.ads.MediaContent
+import com.google.android.gms.ads.nativead.MediaView
+import com.google.android.gms.ads.nativead.NativeAd
+import com.google.android.gms.ads.nativead.NativeAdOptions
+import com.google.android.gms.ads.nativead.NativeAdView
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlin.math.roundToInt
+
+@Composable
+fun NativeAdViewCompose(
+ modifier: Modifier = Modifier,
+ content: @Composable (nativeAdView: NativeAdView) -> Unit
+) = AndroidView(
+ modifier = modifier,
+ factory = {
+ NativeAdView(it)
+ },
+ update = {
+ val composeView = ComposeView(it.context)
+ it.removeAllViews()
+ it.addView(composeView)
+ composeView.setContent {
+ content(it)
+ }
+ }
+)
+
+@Composable
+fun NativeAdView(
+ modifier: Modifier = Modifier,
+ getView: (ComposeView) -> Unit,
+ content: @Composable () -> Unit
+) = AndroidView(
+ modifier = modifier,
+ factory = {
+ ComposeView(it)
+ },
+ update = {
+ it.setContent {
+ content()
+ }
+ getView(it)
+ }
+)
+
+@Composable
+fun NativeAdImage(
+ modifier: Modifier = Modifier,
+ model: Any?,
+ contentDescription: String?,
+ alignment: Alignment = Alignment.Center,
+ contentScale: ContentScale = ContentScale.Fit,
+ alpha: Float = DefaultAlpha,
+ colorFilter: ColorFilter? = null
+) = AsyncImage(
+ model = model,
+ contentDescription = contentDescription,
+ contentScale = contentScale,
+ alignment = alignment,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ modifier = modifier
+)
+
+@Composable
+fun NativeAdMediaView(
+ modifier: Modifier = Modifier,
+ setup: (MediaView) -> Unit
+) = AndroidView(
+ modifier = modifier,
+ factory = {
+ MediaView(it)
+ },
+ update = {
+ setup(it)
+ }
+)
+
+@Composable
+fun NativeAdMediaView(
+ modifier: Modifier = Modifier,
+ nativeAdView: NativeAdView,
+ mediaContent: MediaContent,
+ scaleType: ScaleType = ScaleType.FIT_CENTER
+) = AndroidView(
+ modifier = modifier,
+ factory = {
+ MediaView(it).apply {
+ minimumHeight = 120.dp.value.roundToInt()
+ minimumWidth = 120.dp.value.roundToInt()
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ }
+ },
+ update = {
+ nativeAdView.mediaView = it
+ nativeAdView.mediaView?.mediaContent = mediaContent
+ nativeAdView.mediaView?.setImageScaleType(scaleType)
+ }
+)
+
+@Composable
+fun rememberCustomNativeAdState(
+ adUnit: String,
+ adListener: AdListener? = null,
+ nativeAdOptions: NativeAdOptions? = null
+) = LocalContext.current.let {
+ remember(adUnit) {
+ NativeAdState(
+ context = it,
+ adUnit = adUnit,
+ adListener = adListener,
+ adOptions = nativeAdOptions
+ )
+ }
+}
+
+class NativeAdState(
+ context: Context,
+ adUnit: String,
+ adListener: AdListener?,
+ adOptions: NativeAdOptions?
+) {
+
+ val nativeAd: MutableStateFlow = MutableStateFlow(null)
+
+ init {
+ AdLoader.Builder(context, adUnit).let {
+ if (adOptions != null) {
+ it.withNativeAdOptions(adOptions)
+ } else {
+ it
+ }
+ }.let {
+ if (adListener != null) {
+ it.withAdListener(adListener)
+ } else {
+ it
+ }
+ }.forNativeAd { ad ->
+ nativeAd.update { ad }
+ }.build().loadAd(AdRequest.Builder().build())
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.kt
new file mode 100644
index 0000000..a2c7b45
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.kt
@@ -0,0 +1,14 @@
+package dev.datlag.aniflow.other
+
+import androidx.compose.runtime.staticCompositionLocalOf
+
+expect class ConsentInfo {
+
+ val privacy: Boolean
+
+ fun initialize()
+ fun reset()
+ fun showPrivacyForm()
+}
+
+val LocalConsentInfo = staticCompositionLocalOf { error("No consent info provided") }
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt
index 201932d..76f1f14 100644
--- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt
@@ -4,6 +4,8 @@ data object Constants {
const val GITHUB_REPO = "https://github.com/DatL4g/AniFlow"
const val GITHUB_OWNER = "https://github.com/DatL4g"
+ const val PRIVACY_POLICY = "$GITHUB_REPO/blob/master/Privacy_Policy.md"
+ const val TERMS_CONDITIONS = "$GITHUB_REPO/blob/master/Terms_Conditions.md"
const val SPDX_LICENSE_BASE = "https://spdx.org/licenses/"
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.kt
new file mode 100644
index 0000000..de236a8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.kt
@@ -0,0 +1,46 @@
+package dev.datlag.aniflow.ui.custom
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+expect fun AdView(
+ id: String,
+ type: AdType
+)
+
+@Composable
+fun NativeAdView() {
+ Ads.native()?.let {
+ AdView(
+ id = it,
+ type = AdType.Native
+ )
+ }
+}
+
+@Composable
+expect fun BannerAd(
+ id: String,
+ modifier: Modifier = Modifier
+)
+
+@Composable
+fun BannerAd(modifier: Modifier = Modifier) {
+ Ads.banner()?.let {
+ BannerAd(
+ id = it,
+ modifier = modifier
+ )
+ }
+}
+
+expect object Ads {
+ fun native(): String?
+ fun banner(): String?
+}
+
+sealed interface AdType {
+ data object Native : AdType
+ data object Banner : AdType
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt
index dce5c30..e511847 100644
--- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/component/HidingNavigationBar.kt
@@ -6,6 +6,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
@@ -30,6 +31,7 @@ import dev.chrisbanes.haze.materials.HazeMaterials
import dev.datlag.aniflow.LocalHaze
import dev.datlag.aniflow.SharedRes
import dev.datlag.aniflow.other.MaterialSymbols
+import dev.datlag.aniflow.ui.custom.BannerAd
import dev.datlag.aniflow.ui.navigation.RootConfig
import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle
import dev.icerock.moko.resources.compose.painterResource
@@ -51,96 +53,106 @@ fun HidingNavigationBar(
) {
val density = LocalDensity.current
- AnimatedVisibility(
- visible = visible,
- enter = slideInVertically(
- initialOffsetY = {
- with(density) { it.dp.roundToPx() }
- },
- animationSpec = tween(
- easing = LinearOutSlowInEasing,
- durationMillis = 500
- )
- ),
- exit = slideOutVertically(
- targetOffsetY = { it },
- animationSpec = tween(
- easing = LinearOutSlowInEasing,
- durationMillis = 500
- )
- )
+
+ Column(
+ modifier = Modifier.fillMaxWidth()
) {
- NavigationBar(
- modifier = Modifier.hazeChild(
- state = LocalHaze.current,
- style = HazeMaterials.thin(NavigationBarDefaults.containerColor)
- ).fillMaxWidth(),
- containerColor = Color.Transparent,
- contentColor = MaterialTheme.colorScheme.contentColorFor(NavigationBarDefaults.containerColor)
- ) {
- val isDiscover = remember(selected) {
- selected is NavigationBarState.Discover
- }
- val isHome = remember(selected) {
- selected is NavigationBarState.Home
- }
- val isList = remember(selected) {
- selected is NavigationBarState.Favorite
- }
- val isLoggedIn by loggedIn.collectAsStateWithLifecycle(false)
+ val isHome = remember(selected) {
+ selected is NavigationBarState.Home
+ }
- NavigationBarItem(
- onClick = {
- if (!isDiscover) {
- onDiscover()
- }
- },
- selected = isDiscover,
- icon = {
- Icon(
- imageVector = selected.discoverIcon,
- contentDescription = null
- )
+ if (isHome) {
+ BannerAd(modifier = Modifier.fillMaxWidth())
+ }
+
+ AnimatedVisibility(
+ visible = visible,
+ enter = slideInVertically(
+ initialOffsetY = {
+ with(density) { it.dp.roundToPx() }
},
- label = {
- Text(text = stringResource(SharedRes.strings.discover))
- }
+ animationSpec = tween(
+ easing = LinearOutSlowInEasing,
+ durationMillis = 500
+ )
+ ),
+ exit = slideOutVertically(
+ targetOffsetY = { it },
+ animationSpec = tween(
+ easing = LinearOutSlowInEasing,
+ durationMillis = 500
+ )
)
- NavigationBarItem(
- onClick = {
- if (!isHome) {
- onHome()
- }
- },
- selected = isHome,
- icon = {
- Icon(
- imageVector = selected.homeIcon,
- contentDescription = null
- )
- },
- label = {
- Text(text = stringResource(SharedRes.strings.home))
+ ) {
+ NavigationBar(
+ modifier = Modifier.hazeChild(
+ state = LocalHaze.current,
+ style = HazeMaterials.thin(NavigationBarDefaults.containerColor)
+ ).fillMaxWidth(),
+ containerColor = Color.Transparent,
+ contentColor = MaterialTheme.colorScheme.contentColorFor(NavigationBarDefaults.containerColor)
+ ) {
+ val isDiscover = remember(selected) {
+ selected is NavigationBarState.Discover
}
- )
- NavigationBarItem(
- onClick = {
- if (!isList) {
- onList(isLoggedIn)
- }
- },
- selected = isList,
- enabled = isLoggedIn || listClickable,
- icon = {
- Icon(
- imageVector = selected.favoriteIcon,
- contentDescription = null
- )
- },
- label = {
- Text(text = stringResource(SharedRes.strings.list))
+ val isList = remember(selected) {
+ selected is NavigationBarState.Favorite
}
- )
+ val isLoggedIn by loggedIn.collectAsStateWithLifecycle(false)
+
+ NavigationBarItem(
+ onClick = {
+ if (!isDiscover) {
+ onDiscover()
+ }
+ },
+ selected = isDiscover,
+ icon = {
+ Icon(
+ imageVector = selected.discoverIcon,
+ contentDescription = null
+ )
+ },
+ label = {
+ Text(text = stringResource(SharedRes.strings.discover))
+ }
+ )
+ NavigationBarItem(
+ onClick = {
+ if (!isHome) {
+ onHome()
+ }
+ },
+ selected = isHome,
+ icon = {
+ Icon(
+ imageVector = selected.homeIcon,
+ contentDescription = null
+ )
+ },
+ label = {
+ Text(text = stringResource(SharedRes.strings.home))
+ }
+ )
+ NavigationBarItem(
+ onClick = {
+ if (!isList) {
+ onList(isLoggedIn)
+ }
+ },
+ selected = isList,
+ enabled = isLoggedIn || listClickable,
+ icon = {
+ Icon(
+ imageVector = selected.favoriteIcon,
+ contentDescription = null
+ )
+ },
+ label = {
+ Text(text = stringResource(SharedRes.strings.list))
+ }
+ )
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/discover/DiscoverScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/discover/DiscoverScreen.kt
index 0f8b46e..7c189f0 100644
--- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/discover/DiscoverScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/discover/DiscoverScreen.kt
@@ -56,11 +56,13 @@ import dev.datlag.aniflow.common.scrollUpVisible
import dev.datlag.aniflow.common.title
import dev.datlag.aniflow.other.rememberSearchBarState
import dev.datlag.aniflow.ui.custom.ErrorContent
+import dev.datlag.aniflow.ui.custom.NativeAdView
import dev.datlag.aniflow.ui.navigation.screen.component.HidingNavigationBar
import dev.datlag.aniflow.ui.navigation.screen.component.MediumCard
import dev.datlag.aniflow.ui.navigation.screen.component.NavigationBarState
import dev.datlag.aniflow.ui.navigation.screen.discover.component.DiscoverSearchBar
import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle
+import dev.datlag.tooling.safeSubList
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -159,6 +161,12 @@ fun DiscoverScreen(component: DiscoverComponent) {
}
is DiscoverState.Success -> {
val titleLanguage by component.titleLanguage.collectAsStateWithLifecycle(null)
+ val headList = remember(current.collection) {
+ current.collection.safeSubList(0, 10)
+ }
+ val tailList = remember(current.collection) {
+ current.collection.safeSubList(10, current.collection.size)
+ }
LazyVerticalGrid(
state = listState,
@@ -168,7 +176,18 @@ fun DiscoverScreen(component: DiscoverComponent) {
horizontalArrangement = Arrangement.spacedBy(16.dp),
columns = GridCells.Adaptive(120.dp)
) {
- items(current.collection.toList(), key = { it.id }) {
+ items(headList, key = { it.id }) {
+ MediumCard(
+ medium = it,
+ titleLanguage = titleLanguage,
+ modifier = Modifier.fillMaxWidth().aspectRatio(0.65F),
+ onClick = component::details
+ )
+ }
+ header {
+ NativeAdView()
+ }
+ items(tailList, key = { it.id }) {
MediumCard(
medium = it,
titleLanguage = titleLanguage,
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt
index 64148a4..80b116b 100644
--- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/HomeScreen.kt
@@ -43,6 +43,7 @@ import dev.datlag.aniflow.LocalHaze
import dev.datlag.aniflow.SharedRes
import dev.datlag.aniflow.anilist.type.MediaType
import dev.datlag.aniflow.common.*
+import dev.datlag.aniflow.other.LocalConsentInfo
import dev.datlag.aniflow.other.StateSaver
import dev.datlag.aniflow.other.rememberImagePickerState
import dev.datlag.aniflow.trace.TraceRepository
@@ -73,6 +74,11 @@ fun HomeScreen(component: HomeComponent) {
}
val dialogState by component.dialog.subscribeAsState()
+ val consentInfo = LocalConsentInfo.current
+
+ LaunchedEffect(consentInfo) {
+ consentInfo.initialize()
+ }
dialogState.child?.instance?.render()
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt
index 14ae2b4..14e2db0 100644
--- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/SettingsDialog.kt
@@ -156,6 +156,11 @@ fun SettingsScreen(component: SettingsComponent) {
onClick = component::about
)
}
+ item {
+ PrivacySection(
+ modifier = Modifier.fillParentMaxWidth().padding(top = 16.dp, bottom = 4.dp)
+ )
+ }
}
}
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/PrivacySection.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/PrivacySection.kt
new file mode 100644
index 0000000..d03e3c7
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/PrivacySection.kt
@@ -0,0 +1,55 @@
+package dev.datlag.aniflow.ui.navigation.screen.home.dialog.settings.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.unit.dp
+import dev.datlag.aniflow.SharedRes
+import dev.datlag.aniflow.other.Constants
+import dev.datlag.aniflow.other.LocalConsentInfo
+import dev.icerock.moko.resources.compose.stringResource
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun PrivacySection(
+ modifier: Modifier = Modifier
+) {
+ FlowRow(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically),
+ horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
+ ) {
+ val consentInfo = LocalConsentInfo.current
+ val uriHandler = LocalUriHandler.current
+
+ if (consentInfo.privacy) {
+ TextButton(
+ onClick = {
+ consentInfo.showPrivacyForm()
+ }
+ ) {
+ Text(text = stringResource(SharedRes.strings.edit_consent))
+ }
+ }
+ TextButton(
+ onClick = {
+ uriHandler.openUri(Constants.PRIVACY_POLICY)
+ }
+ ) {
+ Text(text = stringResource(SharedRes.strings.policy))
+ }
+ TextButton(
+ onClick = {
+ uriHandler.openUri(Constants.TERMS_CONDITIONS)
+ }
+ ) {
+ Text(text = stringResource(SharedRes.strings.terms_conditions))
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/moko-resources/base/strings.xml b/composeApp/src/commonMain/moko-resources/base/strings.xml
index 7fc2921..378d789 100644
--- a/composeApp/src/commonMain/moko-resources/base/strings.xml
+++ b/composeApp/src/commonMain/moko-resources/base/strings.xml
@@ -123,4 +123,7 @@
Ignore
Enable In-App View
When enabling this option you can view future shared links directly in this app.
+ Edit Consent
+ Policy
+ Terms & Conditions
diff --git a/composeApp/src/commonMain/moko-resources/de-DE/strings.xml b/composeApp/src/commonMain/moko-resources/de-DE/strings.xml
index 733a65b..77c7b54 100644
--- a/composeApp/src/commonMain/moko-resources/de-DE/strings.xml
+++ b/composeApp/src/commonMain/moko-resources/de-DE/strings.xml
@@ -123,4 +123,7 @@
Ignorieren
In-App Ansicht aktivieren
Wenn du diese Option einschaltest kannst du in Zukunft Links direkt in der App öffnen.
+ Einwilligung bearbeiten
+ Policy
+ Terms & Conditions
diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.ios.kt
new file mode 100644
index 0000000..76bed45
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.ios.kt
@@ -0,0 +1,13 @@
+package dev.datlag.aniflow.other
+
+actual class ConsentInfo {
+ actual val privacy: Boolean
+ get() = false
+
+ actual fun initialize() { }
+
+ actual fun reset() { }
+
+ actual fun showPrivacyForm() { }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.ios.kt
new file mode 100644
index 0000000..f857648
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.ios.kt
@@ -0,0 +1,22 @@
+package dev.datlag.aniflow.ui.custom
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+actual fun BannerAd(id: String, modifier: Modifier) {
+}
+
+@Composable
+actual fun AdView(id: String, type: AdType) {
+}
+
+actual object Ads {
+ actual fun native(): String? {
+ return null
+ }
+
+ actual fun banner(): String? {
+ return null
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3334f7b..3acfd1b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,6 +2,7 @@
app = "1.0.0"
aboutlibraries = "11.2.0"
activity = "1.9.0"
+ads = "23.0.0"
android = "8.4.1"
android-core = "1.13.1"
android-credentials = "1.3.0-alpha04"
@@ -48,6 +49,7 @@ serialization = "1.6.3"
splashscreen = "1.0.1"
tooling = "1.4.0"
translate = "17.0.2"
+ump = "2.2.0"
versions = "0.51.0"
windowsize = "0.5.0"
@@ -58,6 +60,7 @@ android-credentials = { group = "androidx.credentials", name = "credentials", ve
android-credentials-play-services = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "android-credentials" }
activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
+ads = { group = "com.google.android.gms", name = "play-services-ads", version.ref = "ads" }
apollo = { group = "com.apollographql.apollo3", name = "apollo-runtime", version.ref = "apollo" }
apollo-cache = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache", version.ref = "apollo" }
apollo-cache-sql = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache-sqlite", version.ref = "apollo" }
@@ -118,6 +121,7 @@ tooling = { group = "dev.datlag.tooling", name = "tooling-async", version.ref =
tooling-country = { group = "dev.datlag.tooling", name = "tooling-country", version.ref = "tooling" }
tooling-decompose = { group = "dev.datlag.tooling", name = "tooling-decompose", version.ref = "tooling" }
translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
+ump = { group = "com.google.android.ump", name = "user-messaging-platform", version.ref = "ump" }
windowsize = { group = "dev.chrisbanes.material3", name = "material3-window-size-class-multiplatform", version.ref = "windowsize" }
[plugins]