diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/about/AboutScreen.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/about/AboutScreen.kt
index 0b979aed..8b619074 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/about/AboutScreen.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/about/AboutScreen.kt
@@ -18,7 +18,6 @@
package com.sadellie.unitto.feature.settings.about
-import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.lazy.LazyColumn
@@ -42,44 +41,31 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.BuildConfig
import com.sadellie.unitto.core.base.R
-import com.sadellie.unitto.core.ui.common.NavigateUpButton
-import com.sadellie.unitto.core.ui.common.EmptyScreen
import com.sadellie.unitto.core.ui.common.ListItem
+import com.sadellie.unitto.core.ui.common.NavigateUpButton
import com.sadellie.unitto.core.ui.common.ScaffoldWithLargeTopBar
import com.sadellie.unitto.core.ui.openLink
-import com.sadellie.unitto.core.ui.showToast
-import com.sadellie.unitto.data.model.userprefs.AboutPreferences
-import com.sadellie.unitto.data.userprefs.AboutPreferencesImpl
@Composable
internal fun AboutRoute(
- viewModel: AboutViewModel = hiltViewModel(),
navigateUpAction: () -> Unit,
navigateToThirdParty: () -> Unit,
+ navigateToEasterEgg: () -> Unit,
) {
- when (val prefs = viewModel.prefs.collectAsStateWithLifecycle().value) {
- null -> EmptyScreen()
- else -> {
- AboutScreen(
- prefs = prefs,
- navigateUpAction = navigateUpAction,
- navigateToThirdParty = navigateToThirdParty,
- enableToolsExperiment = viewModel::enableToolsExperiment
- )
- }
- }
+ AboutScreen(
+ navigateUpAction = navigateUpAction,
+ navigateToThirdParty = navigateToThirdParty,
+ navigateToEasterEgg = navigateToEasterEgg
+ )
}
@Composable
private fun AboutScreen(
- prefs: AboutPreferences,
navigateUpAction: () -> Unit,
navigateToThirdParty: () -> Unit,
- enableToolsExperiment: () -> Unit,
+ navigateToEasterEgg: () -> Unit,
) {
val mContext = LocalContext.current
var aboutItemClick: Int by rememberSaveable { mutableIntStateOf(0) }
@@ -157,16 +143,11 @@ private fun AboutScreen(
headlineText = stringResource(R.string.settings_version_name),
supportingText = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
modifier = Modifier.combinedClickable {
- if (prefs.enableToolsExperiment) {
- showToast(mContext, "Experiments features are already enabled!", Toast.LENGTH_LONG)
- return@combinedClickable
- }
-
aboutItemClick++
- if (aboutItemClick < 7) return@combinedClickable
-
- enableToolsExperiment()
- showToast(mContext, "Experimental features enabled!", Toast.LENGTH_LONG)
+ if (aboutItemClick > 5) {
+ aboutItemClick = 0
+ navigateToEasterEgg()
+ }
}
)
}
@@ -195,11 +176,8 @@ private fun AboutScreen(
@Composable
fun PreviewAboutScreen() {
AboutScreen(
- prefs = AboutPreferencesImpl(
- enableToolsExperiment = false
- ),
navigateUpAction = {},
navigateToThirdParty = {},
- enableToolsExperiment = {}
+ navigateToEasterEgg = {}
)
}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/about/AboutViewModel.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/about/AboutViewModel.kt
deleted file mode 100644
index bf6c145b..00000000
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/about/AboutViewModel.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Unitto is a calculator for Android
- * Copyright (c) 2023-2024 Elshan Agaev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.sadellie.unitto.feature.settings.about
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.sadellie.unitto.data.common.stateIn
-import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-@HiltViewModel
-internal class AboutViewModel @Inject constructor(
- private val userPrefsRepository: UserPreferencesRepository,
-) : ViewModel() {
- val prefs = userPrefsRepository.aboutPrefs
- .stateIn(viewModelScope, null)
-
- fun enableToolsExperiment() = viewModelScope.launch {
- userPrefsRepository.updateToolsExperiment(true)
- }
-}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/bouncingemoji/BouncingEmoji.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/bouncingemoji/BouncingEmoji.kt
new file mode 100644
index 00000000..4bbb2706
--- /dev/null
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/bouncingemoji/BouncingEmoji.kt
@@ -0,0 +1,243 @@
+/*
+ * Unitto is a calculator for Android
+ * Copyright (c) 2024 Elshan Agaev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.sadellie.unitto.feature.settings.bouncingemoji
+
+import androidx.annotation.FloatRange
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.sadellie.unitto.core.ui.common.NavigateUpButton
+import com.sadellie.unitto.core.ui.common.ScaffoldWithTopBar
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
+import kotlin.random.Random
+
+@Composable
+internal fun BouncingEmojiRoute(
+ navigateUpAction: () -> Unit,
+) {
+ ScaffoldWithTopBar(
+ title = { AnimatedText("Bouncy boy") },
+ navigationIcon = { NavigateUpButton(navigateUpAction) }
+ ) { paddingValues ->
+ BouncingEmojiScreen(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
+ )
+ }
+}
+
+/**
+ * @param modifier [Modifier] that will be applied to the surrounding [BoxWithConstraints].
+ * @param initialX Initial horizontal position. 0 means left, 1 means right.
+ * @param initialY Initial vertical position. 0 means top, 1 means bottom.
+ */
+@Composable
+private fun BouncingEmojiScreen(
+ modifier: Modifier,
+ @FloatRange(0.0, 1.0) initialX: Float = Random.nextFloat(),
+ @FloatRange(0.0, 1.0) initialY: Float = Random.nextFloat(),
+) {
+ val density = LocalDensity.current
+ var speed by remember { mutableFloatStateOf(1f) }
+
+ CompositionLocalProvider(
+ value = LocalDensity provides Density(density.density, fontScale = 1f)
+ ) {
+ BoxWithConstraints(
+ modifier = modifier.clickable {
+ speed = Random.nextFloat()
+ }
+ ) {
+ val width = constraints.maxWidth
+ val height = constraints.maxHeight
+ val ballSize = 96.dp
+ val ballSizePx = with(LocalDensity.current) { ballSize.toPx().roundToInt() }
+
+ var x by rememberSaveable { mutableFloatStateOf((width - ballSizePx) * initialX) }
+ var y by rememberSaveable { mutableFloatStateOf((height - ballSizePx) * initialY) }
+
+ val animatedX = animateFloatAsState(x)
+ val animatedY = animateFloatAsState(y)
+
+ var xSpeed by rememberSaveable { mutableFloatStateOf(10f) }
+ var ySpeed by rememberSaveable { mutableFloatStateOf(10f) }
+
+ var bounces by remember { mutableFloatStateOf(0f) }
+ var edgeHits by rememberSaveable { mutableFloatStateOf(0f) }
+ var emoji by remember { mutableStateOf("❤") }
+ var mood by remember { mutableStateOf("prepare for impact") }
+
+ Column(
+ modifier = Modifier.offset {
+ IntOffset(
+ animatedX.value.roundToInt(),
+ animatedY.value.roundToInt()
+ )
+ },
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .background(MaterialTheme.colorScheme.primaryContainer, CircleShape)
+ .size(ballSize),
+ contentAlignment = Alignment.Center
+ ) {
+ AnimatedText(emoji, MaterialTheme.typography.displayMedium)
+ }
+ AnimatedText(mood)
+ }
+
+ Column {
+ AnimatedText("Speed: ${xSpeed.absoluteValue * speed}")
+ AnimatedText("Bounces: $bounces")
+ AnimatedText("Edge hits: $edgeHits")
+ AnimatedText("Luck: ${edgeHits / bounces}")
+ }
+
+ LaunchedEffect(
+ key1 = Unit,
+ key2 = speed
+ ) {
+ while (isActive) {
+ x += xSpeed * speed
+ y += ySpeed * speed
+
+ val rightBounce = x > width - ballSizePx
+ val leftBounce = x < 0
+ val bottomBounce = y > height - ballSizePx
+ val topBounce = y < 0
+ var bouncedEdges = 0
+
+ if (rightBounce || leftBounce) {
+ xSpeed = -xSpeed
+ bouncedEdges++
+ }
+
+ if (topBounce || bottomBounce) {
+ ySpeed = -ySpeed
+ bouncedEdges++
+ }
+
+ // Count edge hit as 1 bounce
+ when(bouncedEdges) {
+ 2 -> {
+ edgeHits++
+ bounces++
+ mood = winnerMood.random()
+ emoji = winnerEmoji.random()
+ }
+ 1 -> {
+ bounces++
+ mood = looserMood.random()
+ emoji = looserEmoji.random()
+ }
+ }
+
+ delay(1)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AnimatedText(
+ text: String,
+ style: TextStyle = LocalTextStyle.current
+) {
+ AnimatedContent(
+ targetState = text,
+ transitionSpec = {
+ slideInVertically { height -> height } + fadeIn() togetherWith
+ slideOutVertically { height -> -height } + fadeOut()
+ }
+ ) {
+ Text(
+ text = it,
+ style = style,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ }
+}
+
+private val looserEmoji by lazy {
+ listOf("🤡", "😭", "👿", "💀", "💩")
+}
+
+private val winnerEmoji by lazy {
+ listOf("🤠", "🤑", "😎", "🥇")
+}
+
+private val looserMood by lazy {
+ listOf("sus", "bruh", "no cap", "fr fr", "vibing", "oof", "F", "mood", "L+ratio", "yeet")
+}
+
+private val winnerMood by lazy {
+ listOf("ayoo", "W", "skill", "sheeesh", "bro is cheating")
+}
+
+@Preview
+@Composable
+private fun PreviewBouncingEmojiScreen() {
+ BouncingEmojiScreen(
+ modifier = Modifier
+ .background(MaterialTheme.colorScheme.background)
+ .size(400.dp, 550.dp),
+ initialX = 0.5f,
+ initialY = 0.5f,
+ )
+}
diff --git a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt
index 1229e1fe..2d979b94 100644
--- a/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt
+++ b/feature/settings/src/main/java/com/sadellie/unitto/feature/settings/navigation/SettingsNavigation.kt
@@ -27,6 +27,7 @@ import com.sadellie.unitto.core.ui.unittoNavigation
import com.sadellie.unitto.core.ui.unittoStackedComposable
import com.sadellie.unitto.feature.settings.SettingsRoute
import com.sadellie.unitto.feature.settings.about.AboutRoute
+import com.sadellie.unitto.feature.settings.bouncingemoji.BouncingEmojiRoute
import com.sadellie.unitto.feature.settings.calculator.CalculatorSettingsRoute
import com.sadellie.unitto.feature.settings.converter.ConverterSettingsRoute
import com.sadellie.unitto.feature.settings.display.DisplayRoute
@@ -48,6 +49,7 @@ internal const val aboutRoute = "about_route"
internal const val formattingRoute = "formatting_route"
internal const val calculatorSettingsRoute = "calculator_settings_route"
internal const val converterSettingsRoute = "converter_settings_route"
+internal const val bouncingEmoji = "bouncing_emoji_route"
fun NavController.navigateToSettings() {
navigate(DrawerItem.Settings.start)
@@ -123,7 +125,8 @@ fun NavGraphBuilder.settingGraph(
unittoStackedComposable(aboutRoute) {
AboutRoute(
navigateUpAction = navController::navigateUp,
- navigateToThirdParty = { navController.navigate(thirdPartyRoute) }
+ navigateToThirdParty = { navController.navigate(thirdPartyRoute) },
+ navigateToEasterEgg = { navController.navigate(bouncingEmoji) },
)
}
@@ -132,5 +135,11 @@ fun NavGraphBuilder.settingGraph(
navigateUpAction = navController::navigateUp,
)
}
+
+ unittoStackedComposable(bouncingEmoji) {
+ BouncingEmojiRoute(
+ navigateUpAction = navController::navigateUp
+ )
+ }
}
}