diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b1f59d7..7fc522b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "com.bunbeauty.fakelivestream" minSdk = 27 targetSdk = 34 - versionCode = 140 - versionName = "1.4.0" + versionCode = 152 + versionName = "1.5.2" multiDexEnabled = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -27,7 +27,7 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/analytics/AnalyticsManager.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/analytics/AnalyticsManager.kt index 426e174..2540f1b 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/common/analytics/AnalyticsManager.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/common/analytics/AnalyticsManager.kt @@ -17,6 +17,8 @@ private const val STREAM_DURATION_PARAM = "stream_duration" private const val FEEDBACK_POSITIVE_EVENT = "feedback_positive" private const val FEEDBACK_NEGATIVE_EVENT = "feedback_negative" +private const val SHARE_EVENT = "share" + @Singleton class AnalyticsManager @Inject constructor( private val firebaseAnalytics: FirebaseAnalytics @@ -55,4 +57,8 @@ class AnalyticsManager @Inject constructor( } } + fun trackShare() { + firebaseAnalytics.logEvent(SHARE_EVENT) {} + } + } \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/data/SharedPreferencesStorage.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/data/SharedPreferencesStorage.kt index b6b7911..61f1fb0 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/common/data/SharedPreferencesStorage.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/common/data/SharedPreferencesStorage.kt @@ -13,6 +13,7 @@ private const val IMAGE_URI_KEY = "image uri" private const val USERNAME_KEY = "username" private const val VIEWER_COUNT_INDEX_KEY = "viewer count index" private const val SHOULD_ASK_FEEDBACK_KEY = "should ask feedback" +private const val IS_INTRO_VIEWED = "is intro viewed" class SharedPreferencesStorage @Inject constructor( @ApplicationContext private val context: Context @@ -46,6 +47,12 @@ class SharedPreferencesStorage @Inject constructor( } } + override suspend fun saveIsIntroViewed(isIntroViewed: Boolean) { + sharedPreferences.edit { + putBoolean(IS_INTRO_VIEWED, isIntroViewed) + } + } + override fun getImageUriFlow(): Flow { return mutableImageUriFlow.asStateFlow() } @@ -62,6 +69,10 @@ class SharedPreferencesStorage @Inject constructor( return sharedPreferences.getBoolean(SHOULD_ASK_FEEDBACK_KEY, defaultValue) } + override suspend fun getIsIntroViewed(defaultValue: Boolean): Boolean { + return sharedPreferences.getBoolean(IS_INTRO_VIEWED, defaultValue) + } + private fun getImageUri(): String? { return sharedPreferences.getString(IMAGE_URI_KEY, null) } diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/domain/KeyValueStorage.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/domain/KeyValueStorage.kt index dcb8457..7e7375a 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/common/domain/KeyValueStorage.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/common/domain/KeyValueStorage.kt @@ -8,10 +8,12 @@ interface KeyValueStorage { suspend fun saveUsername(username: String) suspend fun saveViewerCountIndex(index: Int) suspend fun saveShouldAskFeedback(shouldAsk: Boolean) + suspend fun saveIsIntroViewed(isIntroViewed: Boolean) fun getImageUriFlow(): Flow suspend fun getUsername(): String? suspend fun getViewerCountIndex(defaultValue: Int): Int suspend fun getShouldAskFeedback(defaultValue: Boolean): Boolean + suspend fun getIsIntroViewed(defaultValue: Boolean): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/navigation/NavigationDestinations.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/navigation/NavigationDestinations.kt index 6e3b686..afb9e9d 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/common/navigation/NavigationDestinations.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/common/navigation/NavigationDestinations.kt @@ -1,6 +1,7 @@ package com.bunbeauty.fakelivestream.common.navigation object NavigationDestinations { + const val INTRO = "intro" const val PREPARATION = "preparation" const val STREAM = "stream" } \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/components/button/PrimaryButton.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/components/button/PrimaryButton.kt index f0370f1..bff5f0f 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/components/button/PrimaryButton.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/components/button/PrimaryButton.kt @@ -11,13 +11,13 @@ import com.bunbeauty.fakelivestream.common.ui.theme.FakeLiveStreamTheme @Composable fun PrimaryButton( - onClick: () -> Unit, text: String, + onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( modifier = modifier, - shape = RoundedCornerShape(32.dp), + shape = RoundedCornerShape(6.dp), colors = ButtonDefaults.buttonColors(containerColor = FakeLiveStreamTheme.colors.interactive), onClick = onClick, ) { diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/theme/Type.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/theme/Type.kt index 0ca2a82..e66b983 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/theme/Type.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/common/ui/theme/Type.kt @@ -43,10 +43,15 @@ data class FakeLiveStreamTypography( fontSize = 14.sp, lineHeight = 20.sp ), -// val action: TextStyle = TextStyle( -// fontFamily = FontFamily(Font(R.font.roboto_medium)), -// fontWeight = FontWeight.W500, -// fontSize = 14.sp, -// lineHeight = 20.sp -// ), -) \ No newline at end of file + val bodyLarge: TextStyle = TextStyle( + fontFamily = FontFamily(Font(R.font.roboto_regular)), + fontWeight = FontWeight.W400, + fontSize = 16.sp, + lineHeight = 22.sp + ), +) + +val TextStyle.bold: TextStyle + get() { + return copy(fontWeight = FontWeight.Bold) + } \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/util/ActivityExtensions.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/util/ActivityExtensions.kt new file mode 100644 index 0000000..401f93c --- /dev/null +++ b/app/src/main/java/com/bunbeauty/fakelivestream/common/util/ActivityExtensions.kt @@ -0,0 +1,41 @@ +package com.bunbeauty.fakelivestream.common.util + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import com.google.android.play.core.review.ReviewManagerFactory + +fun Activity.launchInAppReview() { + val reviewManager = ReviewManagerFactory.create(this) + val request = reviewManager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + reviewManager.launchReviewFlow(this, task.result) + } + } +} + +fun Activity.openSettings() { + startActivity( + Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.fromParts("package", packageName, null) + } + ) +} + +fun Activity.openSharing(text: String): Boolean { + return try { + val intent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + } + val chooser = Intent.createChooser(intent, null) + startActivity(chooser) + true + } catch (exception: Exception) { + false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/common/util/InAppReviewExtensions.kt b/app/src/main/java/com/bunbeauty/fakelivestream/common/util/InAppReviewExtensions.kt deleted file mode 100644 index da2f78e..0000000 --- a/app/src/main/java/com/bunbeauty/fakelivestream/common/util/InAppReviewExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.bunbeauty.fakelivestream.common.util - -import android.app.Activity -import com.google.android.play.core.review.ReviewManagerFactory - -fun Activity.launchInAppReview() { - val reviewManager = ReviewManagerFactory.create(this) - val request = reviewManager.requestReviewFlow() - request.addOnCompleteListener { task -> - if (task.isSuccessful) { - reviewManager.launchReviewFlow(this, task.result) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/domain/CheckIsIntroSeenUseCase.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/domain/CheckIsIntroSeenUseCase.kt new file mode 100644 index 0000000..00b47bc --- /dev/null +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/domain/CheckIsIntroSeenUseCase.kt @@ -0,0 +1,14 @@ +package com.bunbeauty.fakelivestream.features.intro.domain + +import com.bunbeauty.fakelivestream.common.domain.KeyValueStorage +import javax.inject.Inject + +class CheckIsIntroViewedUseCase @Inject constructor( + private val keyValueStorage: KeyValueStorage +) { + + suspend operator fun invoke(): Boolean { + return keyValueStorage.getIsIntroViewed( defaultValue = false) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/domain/SaveIsIntroSeenUseCase.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/domain/SaveIsIntroSeenUseCase.kt new file mode 100644 index 0000000..b444e51 --- /dev/null +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/domain/SaveIsIntroSeenUseCase.kt @@ -0,0 +1,14 @@ +package com.bunbeauty.fakelivestream.features.intro.domain + +import com.bunbeauty.fakelivestream.common.domain.KeyValueStorage +import javax.inject.Inject + +class SaveIsIntroSeenUseCase @Inject constructor( + private val keyValueStorage: KeyValueStorage +) { + + suspend operator fun invoke(isIntroViewed: Boolean) { + return keyValueStorage.saveIsIntroViewed(isIntroViewed = isIntroViewed) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/presentation/Intro.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/presentation/Intro.kt new file mode 100644 index 0000000..0c5c0ab --- /dev/null +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/presentation/Intro.kt @@ -0,0 +1,18 @@ +package com.bunbeauty.fakelivestream.features.intro.presentation + +import com.bunbeauty.fakelivestream.common.presentation.Base + +interface Intro { + + data class State( + val isChecked: Boolean + ) : Base.State + + sealed interface Action : Base.Action { + data object NextClick : Action + } + + sealed interface Event : Base.Event { + data object OpenPreparation : Event + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/presentation/IntroViewModel.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/presentation/IntroViewModel.kt new file mode 100644 index 0000000..110f2ed --- /dev/null +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/presentation/IntroViewModel.kt @@ -0,0 +1,44 @@ +package com.bunbeauty.fakelivestream.features.intro.presentation + +import androidx.lifecycle.viewModelScope +import com.bunbeauty.fakelivestream.common.presentation.BaseViewModel +import com.bunbeauty.fakelivestream.features.intro.domain.CheckIsIntroViewedUseCase +import com.bunbeauty.fakelivestream.features.intro.domain.SaveIsIntroSeenUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class IntroViewModel @Inject constructor( + private val checkIsIntroViewedUseCase: CheckIsIntroViewedUseCase, + private val saveIsIntroSeenUseCase: SaveIsIntroSeenUseCase, +) : BaseViewModel( + initState = { + Intro.State(isChecked = false) + } +) { + + init { + viewModelScope.launch { + if (checkIsIntroViewedUseCase()) { + sendEvent(Intro.Event.OpenPreparation) + } else { + setState { + copy(isChecked = true) + } + } + } + } + + override fun onAction(action: Intro.Action) { + when (action) { + Intro.Action.NextClick -> { + viewModelScope.launch { + saveIsIntroSeenUseCase(isIntroViewed = true) + sendEvent(Intro.Event.OpenPreparation) + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/view/IntroScreen.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/view/IntroScreen.kt new file mode 100644 index 0000000..92a0d1e --- /dev/null +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/intro/view/IntroScreen.kt @@ -0,0 +1,141 @@ +package com.bunbeauty.fakelivestream.features.intro.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Divider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import com.bunbeauty.fakelivestream.R +import com.bunbeauty.fakelivestream.common.navigation.NavigationDestinations.INTRO +import com.bunbeauty.fakelivestream.common.navigation.NavigationDestinations.PREPARATION +import com.bunbeauty.fakelivestream.common.ui.LocalePreview +import com.bunbeauty.fakelivestream.common.ui.components.button.PrimaryButton +import com.bunbeauty.fakelivestream.common.ui.theme.FakeLiveStreamTheme +import com.bunbeauty.fakelivestream.common.ui.theme.bold +import com.bunbeauty.fakelivestream.features.intro.presentation.Intro +import com.bunbeauty.fakelivestream.features.intro.presentation.IntroViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@Composable +fun IntroScreen(navController: NavHostController) { + val viewModel: IntroViewModel = hiltViewModel() + val state by viewModel.state.collectAsStateWithLifecycle() + val onAction = remember { + { action: Intro.Action -> + viewModel.onAction(action) + } + } + + val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + viewModel.event.onEach { event -> + when (event) { + is Intro.Event.OpenPreparation -> { + navController.navigate(PREPARATION) { + popUpTo(INTRO) { + inclusive = true + } + } + } + } + }.launchIn(scope) + } + + IntroContent( + isChecked = state.isChecked, + onAction = onAction + ) +} + +@Composable +private fun IntroContent( + isChecked: Boolean, + onAction: (Intro.Action) -> Unit +) { + if (isChecked) { + Column(modifier = Modifier.background(FakeLiveStreamTheme.colors.background)) { + Column( + modifier = Modifier + .padding(16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + verticalArrangement = spacedBy(16.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Row { + Spacer(modifier = Modifier.weight(1f)) + Image( + modifier = Modifier.weight(4f), + painter = painterResource(R.drawable.img_logo), + contentDescription = "Logo" + ) + Spacer(modifier = Modifier.weight(1f)) + } + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource( + id = R.string.intro_welcome, + stringResource(R.string.app_name) + ), + style = FakeLiveStreamTheme.typography.titleLarge.bold, + textAlign = TextAlign.Center + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.intro_description), + style = FakeLiveStreamTheme.typography.bodyLarge, + textAlign = TextAlign.Justify + ) + Spacer(modifier = Modifier.weight(1f)) + } + Divider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = FakeLiveStreamTheme.colors.borderVariant, + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + text = stringResource(id = R.string.intro_next), + onClick = { + onAction(Intro.Action.NextClick) + }, + ) + } + } else { + Box(modifier = Modifier.fillMaxSize()) + } +} + +@LocalePreview +@Composable +fun IntroScreenPreview() { + IntroContent( + isChecked = true, + onAction = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/CropImageDefaults.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/CropImageDefaults.kt new file mode 100644 index 0000000..4ed302e --- /dev/null +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/CropImageDefaults.kt @@ -0,0 +1,22 @@ +package com.bunbeauty.fakelivestream.features.main + +import android.graphics.Color +import com.canhub.cropper.CropImageOptions +import com.canhub.cropper.CropImageView + +object CropImageDefaults { + + fun options(): CropImageOptions { + return CropImageOptions( + imageSourceIncludeCamera = false, + cropShape = CropImageView.CropShape.OVAL, + autoZoomEnabled = false, + fixAspectRatio = true, + toolbarColor = Color.WHITE, + activityBackgroundColor = Color.WHITE, + activityMenuIconColor = Color.BLACK, + activityMenuTextColor = Color.BLACK, + toolbarBackButtonColor = Color.BLACK, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/MainActivity.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/MainActivity.kt index 999eca4..94aa753 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/MainActivity.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/MainActivity.kt @@ -1,11 +1,7 @@ package com.bunbeauty.fakelivestream.features.main import android.Manifest.permission.CAMERA -import android.content.Intent -import android.graphics.Color -import android.net.Uri import android.os.Bundle -import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts @@ -28,11 +24,16 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.bunbeauty.fakelivestream.R +import com.bunbeauty.fakelivestream.common.navigation.NavigationDestinations.INTRO import com.bunbeauty.fakelivestream.common.navigation.NavigationDestinations.PREPARATION import com.bunbeauty.fakelivestream.common.navigation.NavigationDestinations.STREAM import com.bunbeauty.fakelivestream.common.ui.keepScreenOn import com.bunbeauty.fakelivestream.common.ui.theme.FakeLiveStreamTheme import com.bunbeauty.fakelivestream.common.util.launchInAppReview +import com.bunbeauty.fakelivestream.common.util.openSettings +import com.bunbeauty.fakelivestream.common.util.openSharing +import com.bunbeauty.fakelivestream.features.intro.view.IntroScreen import com.bunbeauty.fakelivestream.features.main.presentation.Main import com.bunbeauty.fakelivestream.features.main.presentation.MainViewModel import com.bunbeauty.fakelivestream.features.main.view.CameraIsRequiredDialog @@ -41,12 +42,12 @@ import com.bunbeauty.fakelivestream.features.stream.view.DURATION_NAV_PARAM import com.bunbeauty.fakelivestream.features.stream.view.StreamScreen import com.canhub.cropper.CropImageContract import com.canhub.cropper.CropImageContractOptions -import com.canhub.cropper.CropImageOptions -import com.canhub.cropper.CropImageView import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +private const val GOOGLE_PLAY_LINK = "https://play.google.com/store/apps/details?id=com.bunbeauty.fakelivestream" + @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -85,7 +86,12 @@ class MainActivity : ComponentActivity() { } } if (state.showNoCameraPermission) { - CameraIsRequiredDialog(onAction = onAction) + CameraIsRequiredDialog( + onAction = onAction, + onSettingsClick = { + openSettings() + } + ) } } } @@ -100,30 +106,11 @@ class MainActivity : ComponentActivity() { } } - private fun openSettings() { - startActivity( - Intent().apply { - action = ACTION_APPLICATION_DETAILS_SETTINGS - data = Uri.fromParts("package", packageName, null) - } - ) - } - private fun launchAvatarSetting() { cropImage.launch( CropImageContractOptions( uri = null, - cropImageOptions = CropImageOptions( - imageSourceIncludeCamera = false, - cropShape = CropImageView.CropShape.OVAL, - autoZoomEnabled = false, - fixAspectRatio = true, - toolbarColor = Color.WHITE, - activityBackgroundColor = Color.WHITE, - activityMenuIconColor = Color.BLACK, - activityMenuTextColor = Color.BLACK, - toolbarBackButtonColor = Color.BLACK, - ) + cropImageOptions = CropImageDefaults.options() ) ) } @@ -140,10 +127,6 @@ class MainActivity : ComponentActivity() { LaunchedEffect(Unit) { mainViewModel.event.onEach { event -> when (event) { - Main.Event.OpenSettings -> { - openSettings() - } - Main.Event.OpenStream -> { navController.navigate(STREAM) } @@ -171,7 +154,7 @@ class MainActivity : ComponentActivity() { ) { NavHost( navController = navController, - startDestination = PREPARATION, + startDestination = INTRO, modifier = modifier, enterTransition = { EnterTransition.None @@ -180,6 +163,9 @@ class MainActivity : ComponentActivity() { ExitTransition.None }, ) { + composable(route = INTRO) { + IntroScreen(navController = navController) + } composable(route = PREPARATION) { entry -> val streamDurationInSeconds = entry.savedStateHandle.get(DURATION_NAV_PARAM) PreparationScreen( @@ -190,8 +176,20 @@ class MainActivity : ComponentActivity() { onStartStreamClick = { requestCameraPermission() }, - openInAppReview = { + onPositiveFeedbackClick = { launchInAppReview() + }, + onShareClick = { + val isSuccessful = openSharing( + text = getString( + R.string.sharing_text, + getString(R.string.app_name), + GOOGLE_PLAY_LINK + ), + ) + if (!isSuccessful) { + // TODO show error + } } ) } diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/Main.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/Main.kt index a77c12e..a0d4834 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/Main.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/Main.kt @@ -13,12 +13,10 @@ interface Main { data object CameraPermissionDeny: Action data object CameraPermissionAccept: Action data object CloseCameraRequiredDialogClick: Action - data object SettingsClick: Action data class AvatarSelected(val uri: Uri?): Action } sealed interface Event: Base.Event { - data object OpenSettings: Event data object OpenStream: Event } diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/MainViewModel.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/MainViewModel.kt index c538c58..0029042 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/MainViewModel.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/presentation/MainViewModel.kt @@ -34,10 +34,6 @@ class MainViewModel @Inject constructor( } } - Main.Action.SettingsClick -> { - sendEvent(Main.Event.OpenSettings) - } - is Main.Action.AvatarSelected -> { viewModelScope.launch { action.uri?.let { uri -> diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/view/CameraIsRequiredDialog.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/view/CameraIsRequiredDialog.kt index 0f52ce4..940b7c6 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/features/main/view/CameraIsRequiredDialog.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/main/view/CameraIsRequiredDialog.kt @@ -23,7 +23,8 @@ import com.bunbeauty.fakelivestream.features.main.presentation.Main @OptIn(ExperimentalMaterial3Api::class) @Composable fun CameraIsRequiredDialog( - onAction: (Main.Action) -> Unit + onAction: (Main.Action) -> Unit, + onSettingsClick: () -> Unit, ) { AlertDialog( onDismissRequest = { @@ -57,9 +58,7 @@ fun CameraIsRequiredDialog( .padding(top = 16.dp) .align(Alignment.End), text = stringResource(R.string.settings), - onClick = { - onAction(Main.Action.SettingsClick) - }, + onClick = onSettingsClick, ) } } @@ -68,5 +67,10 @@ fun CameraIsRequiredDialog( @LocalePreview @Composable private fun CameraIsRequiredDialogPreview() { - CameraIsRequiredDialog(onAction = {}) + FakeLiveStreamTheme { + CameraIsRequiredDialog( + onAction = {}, + onSettingsClick = {} + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/Preparation.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/Preparation.kt index 9c093e4..a035cc1 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/Preparation.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/Preparation.kt @@ -24,12 +24,14 @@ interface Preparation { data object CloseFeedbackDialogClick: Action data class FeedbackClick(val isPositive: Boolean): Action data class NotShowFeedbackChecked(val checked: Boolean): Action + data object ShareClick: Action } sealed interface Event: Base.Event { data object OpenStream: Event - data object OpenInAppReview: Event + data object HandlePositiveFeedbackClick: Event data object HandleAvatarClick: Event + data object HandleShareClick: Event } } \ No newline at end of file diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/PreparationViewModel.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/PreparationViewModel.kt index bdef4a1..4bebaab 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/PreparationViewModel.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/presentation/PreparationViewModel.kt @@ -121,7 +121,7 @@ class PreparationViewModel @Inject constructor( } analyticsManager.trackFeedback(action.isPositive) if (action.isPositive) { - sendEvent(Preparation.Event.OpenInAppReview) + sendEvent(Preparation.Event.HandlePositiveFeedbackClick) } } @@ -130,6 +130,10 @@ class PreparationViewModel @Inject constructor( saveShouldAskFeedbackUseCase(shouldAsk = !action.checked) } } + Preparation.Action.ShareClick -> { + analyticsManager.trackShare() + sendEvent(Preparation.Event.HandleShareClick) + } } } diff --git a/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/view/PreparationScreen.kt b/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/view/PreparationScreen.kt index 18f9a20..0aa6f7e 100644 --- a/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/view/PreparationScreen.kt +++ b/app/src/main/java/com/bunbeauty/fakelivestream/features/preparation/view/PreparationScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -24,6 +25,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp @@ -50,7 +52,8 @@ fun PreparationScreen( streamDurationInSeconds: Int?, onAvatarClick: () -> Unit, onStartStreamClick: () -> Unit, - openInAppReview: () -> Unit, + onPositiveFeedbackClick: () -> Unit, + onShareClick: () -> Unit, ) { val viewModel: PreparationViewModel = hiltViewModel() val state by viewModel.state.collectAsStateWithLifecycle() @@ -68,13 +71,17 @@ fun PreparationScreen( onStartStreamClick() } - Preparation.Event.OpenInAppReview -> { - openInAppReview() + Preparation.Event.HandlePositiveFeedbackClick -> { + onPositiveFeedbackClick() } Preparation.Event.HandleAvatarClick -> { onAvatarClick() } + + Preparation.Event.HandleShareClick -> { + onShareClick() + } } }.launchIn(scope) } @@ -96,7 +103,7 @@ fun PreparationScreen( } @Composable -fun PreparationContent( +private fun PreparationContent( state: Preparation.State, onAction: (Preparation.Action) -> Unit ) { @@ -106,6 +113,23 @@ fun PreparationContent( .background(FakeLiveStreamTheme.colors.background) .padding(16.dp) ) { + IconButton( + modifier = Modifier + .align(Alignment.TopEnd) + .clip(RoundedCornerShape(6.dp)) + .background(FakeLiveStreamTheme.colors.interactive) + .size(48.dp), + onClick = { + onAction(Preparation.Action.ShareClick) + } + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_share), + tint = FakeLiveStreamTheme.colors.onSurface, + contentDescription = "share" + ) + } Column(modifier = Modifier.align(Alignment.Center)) { CachedImage( modifier = Modifier diff --git a/app/src/main/res/drawable-hdpi/img_logo.webp b/app/src/main/res/drawable-hdpi/img_logo.webp new file mode 100644 index 0000000..152b207 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/img_logo.webp differ diff --git a/app/src/main/res/drawable-mdpi/img_logo.webp b/app/src/main/res/drawable-mdpi/img_logo.webp new file mode 100644 index 0000000..15f45de Binary files /dev/null and b/app/src/main/res/drawable-mdpi/img_logo.webp differ diff --git a/app/src/main/res/drawable-xhdpi/img_logo.webp b/app/src/main/res/drawable-xhdpi/img_logo.webp new file mode 100644 index 0000000..54a01d3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/img_logo.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/img_logo.webp b/app/src/main/res/drawable-xxhdpi/img_logo.webp new file mode 100644 index 0000000..bc4c7cd Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_logo.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/img_logo.webp b/app/src/main/res/drawable-xxxhdpi/img_logo.webp new file mode 100644 index 0000000..4bd6e29 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/img_logo.webp differ diff --git a/app/src/main/res/drawable/a10.webp b/app/src/main/res/drawable/a10.webp index 739f6ad..74b981b 100644 Binary files a/app/src/main/res/drawable/a10.webp and b/app/src/main/res/drawable/a10.webp differ diff --git a/app/src/main/res/drawable/a48.webp b/app/src/main/res/drawable/a48.webp index 0a486ca..87a5b03 100644 Binary files a/app/src/main/res/drawable/a48.webp and b/app/src/main/res/drawable/a48.webp differ diff --git a/app/src/main/res/drawable/a49.webp b/app/src/main/res/drawable/a49.webp index 4717110..7ea1a71 100644 Binary files a/app/src/main/res/drawable/a49.webp and b/app/src/main/res/drawable/a49.webp differ diff --git a/app/src/main/res/drawable/a67.webp b/app/src/main/res/drawable/a67.webp index c5d7368..fbbaa23 100644 Binary files a/app/src/main/res/drawable/a67.webp and b/app/src/main/res/drawable/a67.webp differ diff --git a/app/src/main/res/drawable/a92.webp b/app/src/main/res/drawable/a92.webp index 2bba6ea..92cffcc 100644 Binary files a/app/src/main/res/drawable/a92.webp and b/app/src/main/res/drawable/a92.webp differ diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..fe42656 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 502a68d..5b6ac4a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,5 +1,12 @@ + + Сontinuar + Bem-vindo ao %s + Colocamos a sua privacidade acima de tudo. Seus dados são sagrados para nós, por isso permanecem confidenciais e seguros. Quando se trata de transmissões falsas, só você tem acesso a elas. Então relaxe, divirta-se e nós cuidaremos do resto! + + Olá! Confira o aplicativo %s no Google Play: %s + AO VIVO %s.%sK Adicione um comentário… diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e874442..fd34b83 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,5 +1,11 @@ + Продолжить + Добро пожаловать в %s + Мы ставим вашу конфиденциальность превыше всего. Ваши данные для нас священны, поэтому они остаются конфиденциальными и безопасными. Когда дело доходит до фейковых трансляций, доступ к ним есть только у вас. Так что расслабьтесь, получайте удовольствие, а обо всем остальном позаботимся мы! + + Привет! Зацени приложение %s в Google Play: %s + ПРЯМОЙ ЭФИР %s,%s тыс. Добавьте комментарий… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5660bb..7a4f7e8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,12 @@ Like Live + Next + Welcome to %s + We prioritize your privacy above all else. Your data is sacred to us, so it stays strictly private and secure. When it comes to fake Lives, only you have access to your own. So, relax, have fun, and let us take care of the rest! + + Hey! Check out %s app on Google Play: %s + LIVE %s.%sK Add comment…