From d32fc1bf056148419513fbf594d3cfddd27ef652 Mon Sep 17 00:00:00 2001 From: DatLag Date: Sun, 26 May 2024 22:24:21 +0200 Subject: [PATCH] added ads --- composeApp/build.gradle.kts | 2 + .../src/androidMain/AndroidManifest.xml | 14 ++ .../kotlin/dev/datlag/aniflow/App.kt | 1 + .../kotlin/dev/datlag/aniflow/MainActivity.kt | 7 +- .../aniflow/other/ConsentInfo.android.kt | 59 ++++++ .../aniflow/ui/custom/AdView.android.kt | 75 +++++++ .../datlag/aniflow/ui/custom/NativeAdCard.kt | 200 ++++++++++++++++++ .../dev/datlag/aniflow/ui/custom/NativeAds.kt | 166 +++++++++++++++ .../dev/datlag/aniflow/other/ConsentInfo.kt | 14 ++ .../dev/datlag/aniflow/other/Constants.kt | 2 + .../dev/datlag/aniflow/ui/custom/AdView.kt | 46 ++++ .../screen/component/HidingNavigationBar.kt | 180 ++++++++-------- .../screen/discover/DiscoverScreen.kt | 21 +- .../ui/navigation/screen/home/HomeScreen.kt | 6 + .../home/dialog/settings/SettingsDialog.kt | 5 + .../settings/component/PrivacySection.kt | 55 +++++ .../moko-resources/base/strings.xml | 3 + .../moko-resources/de-DE/strings.xml | 3 + .../datlag/aniflow/other/ConsentInfo.ios.kt | 13 ++ .../datlag/aniflow/ui/custom/AdView.ios.kt | 22 ++ gradle/libs.versions.toml | 4 + 21 files changed, 812 insertions(+), 86 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.android.kt create mode 100644 composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.android.kt create mode 100644 composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAdCard.kt create mode 100644 composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/custom/NativeAds.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/home/dialog/settings/component/PrivacySection.kt create mode 100644 composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ConsentInfo.ios.kt create mode 100644 composeApp/src/iosMain/kotlin/dev/datlag/aniflow/ui/custom/AdView.ios.kt 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]