diff --git a/.github/release.yml b/.github/release.yml index 48901655..0b643b8a 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -2,23 +2,28 @@ changelog: categories: - - title: Highlights 🌟 + - title: 🌟 Highlights labels: - highlights - - title: New Features 🚀 + - title: 🚀 New Features labels: - feature - enhancement - - title: Bug Fixes 🐛 + - title: 🐛 Bug Fixes labels: - bug - - title: Documentation 📚 + - title: 📚 Documentation labels: - documentation + - title: 📦 Dependency Updates + labels: + - dependencies + - dependabot + - title: Other Changes labels: - "*" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 211192fd..e4980f50 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,7 @@ import java.util.Properties * Gets the current system time formatted as 'yy-mm-dd : hh-mm-ss'. */ fun getCurrentTimeLabel(): String { - val formatter = SimpleDateFormat("yy-MM-dd : HH-mm-ss", Locale.getDefault()) + val formatter = SimpleDateFormat("yy-MM-dd - HH:mm:ss", Locale.getDefault()) return formatter.format(Date()) } @@ -52,6 +52,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.ksp) + alias(libs.plugins.jetbrains.kotlin.serialization) alias(libs.plugins.oss.licenses) } @@ -222,6 +223,10 @@ dependencies { implementation(libs.bundles.compose) debugImplementation(libs.bundles.compose.debug) + // Navigation 3 + implementation(libs.bundles.navigation3) + implementation(libs.kotlinx.serialization.json) + // Google Play License Services implementation(libs.oss.license) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a618b668..08239b0c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools" > @@ -14,11 +14,14 @@ android:roundIcon="${appRoundIcon}" android:supportsRtl="true" android:theme="@style/PasscodesTheme.Default" - tools:targetApi="33"> + tools:targetApi="33" > + + + + android:exported="true" > @@ -32,7 +35,7 @@ + android:exported="false" > diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/JetpackPreviewLayoutActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/JetpackPreviewLayoutActivity.kt new file mode 100644 index 00000000..a00b22ff --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/JetpackPreviewLayoutActivity.kt @@ -0,0 +1,103 @@ +package com.jeeldobariya.passcodes + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.jeeldobariya.passcodes.core.feature_flags.featureFlagsDatastore +import com.jeeldobariya.passcodes.ui.ui.theme.PasscodesTheme +import kotlinx.coroutines.launch +import kotlin.system.exitProcess + +class JetpackPreviewLayoutActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PasscodesTheme { + val composableScope = rememberCoroutineScope() + + JetpackComposeNewUIRoot( + navigateToNewUi = ::navigateToJetpackComposeNavigation, + navigateToOldUi = { + composableScope.launch { + featureFlagsDatastore.updateData { + it.copy(isPreviewLayoutEnabled = false) + } + + finishAndRemoveTask() + exitProcess(0) + } + } + ) + } + } + } + + fun navigateToJetpackComposeNavigation() { + Intent(this@JetpackPreviewLayoutActivity, com.jeeldobariya.passcodes.ui.MainActivity::class.java).also { + startActivity(it) + } + } +} + +@Composable +fun JetpackComposeNewUIRoot(navigateToNewUi: () -> Unit, navigateToOldUi: () -> Unit) { + Surface( + color = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxSize(), + tonalElevation = 5.dp + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Passcodes", fontSize = 40.sp) + Text("You are on New Jetpack UI") + + Spacer(Modifier.padding(32.dp)) + + Button(onClick = navigateToOldUi) { + Text("Back To Old UI", fontSize = 20.sp) + } + + OutlinedButton(onClick = navigateToNewUi) { + Text("Continue To New UI", fontSize = 20.sp) + } + + Spacer(Modifier.padding(12.dp)) + + Text("This UI is Under Active Development", fontSize = 12.sp) + Text("So, There might be small issues!!", fontSize = 12.sp) + } + } +} + +@Preview(showBackground = true) +@Composable +fun BasePreview() { + PasscodesTheme { + JetpackComposeNewUIRoot(navigateToOldUi = { }, navigateToNewUi = { }) + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/navigation/NavigationRoot.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/navigation/NavigationRoot.kt new file mode 100644 index 00000000..6bed8b10 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/navigation/NavigationRoot.kt @@ -0,0 +1,55 @@ +package com.jeeldobariya.passcodes.navigation + +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import com.jeeldobariya.passcodes.core.navigation.Route +import com.jeeldobariya.passcodes.password_manager.ui.PasswordManagerScreen +import com.jeeldobariya.passcodes.password_manager.ui.SavePasswordScreen +import com.jeeldobariya.passcodes.ui.AboutScreen +import com.jeeldobariya.passcodes.ui.MainScreen +import com.jeeldobariya.passcodes.ui.SettingsScreen + +@Composable +fun NavigationRoot() { + val backStack = rememberNavBackStack(Route.Home) + + fun navigateTo(route: Route): Unit { + backStack.add(route) + } + + NavDisplay( + backStack = backStack, + onBack = { + backStack.removeLastOrNull() + }, + entryDecorators = mutableListOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator() + ), + entryProvider = entryProvider { + entry { + MainScreen(::navigateTo) + } + + entry { + SettingsScreen() + } + + entry { + AboutScreen() + } + + entry { + PasswordManagerScreen(::navigateTo) + } + + entry { + SavePasswordScreen() + } + } + ) +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt index cf2fae1b..0728db6a 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/oldui/MainActivity.kt @@ -12,6 +12,7 @@ import com.jeeldobariya.passcodes.core.domain.usecases.CheckForUpdateUseCase import com.jeeldobariya.passcodes.core.feature_flags.featureFlagsDatastore import com.jeeldobariya.passcodes.databinding.ActivityMainBinding import com.jeeldobariya.passcodes.password_manager.oldui.PasswordManagerActivity +import com.jeeldobariya.passcodes.JetpackPreviewLayoutActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -58,11 +59,9 @@ class MainActivity : AppCompatActivity() { runBlocking { if (featureFlagsDatastore.data.first().isPreviewLayoutEnabled) { - val jetpackComposeActivity = Intent( - this@MainActivity, - com.jeeldobariya.passcodes.ui.MainActivity::class.java - ) - startActivity(jetpackComposeActivity) + Intent(this@MainActivity, JetpackPreviewLayoutActivity::class.java).also { + startActivity(it) + } } } } diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/AboutScreen.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/AboutScreen.kt new file mode 100644 index 00000000..7917da3c --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/AboutScreen.kt @@ -0,0 +1,82 @@ +package com.jeeldobariya.passcodes.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.jeeldobariya.passcodes.R +import com.jeeldobariya.passcodes.ui.ui.theme.PasscodesTheme + +@Composable +fun AboutScreen() { + Scaffold { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 24.dp, vertical = 64.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_passcodes), + contentDescription = "Passcodes Icon", + modifier = Modifier + .size(100.dp) + .padding(10.dp) + ) + + Text( + text = stringResource(R.string.textview_aboutus_headline), + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.padding(vertical = 12.dp)) + + Text( + text = stringResource(R.string.textview_app_description), + style = MaterialTheme.typography.bodyLarge + ) + + Spacer(modifier = Modifier.padding(vertical = 16.dp)) + + Text( + text = stringResource(R.string.textview_app_warning), + style = MaterialTheme.typography.labelSmall, + color = Color.Red + ) + + Spacer(modifier = Modifier.padding(16.dp)) + } + } +} + + +@Preview(showBackground = true) +@PreviewLightDark +@Composable +fun AboutScreenPreview() { + PasscodesTheme { + AboutScreen() + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt index b504d923..6890ee6d 100644 --- a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainActivity.kt @@ -4,26 +4,8 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.jeeldobariya.passcodes.core.feature_flags.featureFlagsDatastore +import com.jeeldobariya.passcodes.navigation.NavigationRoot import com.jeeldobariya.passcodes.ui.ui.theme.PasscodesTheme -import kotlinx.coroutines.launch -import kotlin.system.exitProcess class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -31,72 +13,8 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { PasscodesTheme { - MainScreen { - featureFlagsDatastore.updateData { - it.copy(isPreviewLayoutEnabled = false) - } - - finishAndRemoveTask() - exitProcess(0) - } + NavigationRoot() } } } } - -@Composable -fun MainScreen(navigateToOldUi: suspend () -> Unit) { - val scope = rememberCoroutineScope() - - Surface( - color = MaterialTheme.colorScheme.surface, - modifier = Modifier.fillMaxSize(), - tonalElevation = 5.dp - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Passcodes", fontSize = 24.sp) - Text("You are on New Jetpack UI") - - Spacer(Modifier.padding(12.dp)) - - Button( - onClick = { - scope.launch { - navigateToOldUi() - } - } - ) { - Text("Back To Old UI", fontSize = 20.sp) - } - Button( - onClick = { - scope.launch { - navigateToOldUi() - } - } - ) { - Text("Continue New UI", fontSize = 20.sp) - } - - Spacer(Modifier.padding(12.dp)) - - Text("Jetpack UI Is Under Development", fontSize = 11.sp) - } - } -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - PasscodesTheme { - MainScreen( - navigateToOldUi = { } - ) - } -} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainScreen.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainScreen.kt new file mode 100644 index 00000000..0b9933cf --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/MainScreen.kt @@ -0,0 +1,122 @@ +package com.jeeldobariya.passcodes.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.jeeldobariya.passcodes.R +import com.jeeldobariya.passcodes.core.navigation.Route +import com.jeeldobariya.passcodes.ui.ui.theme.PasscodesTheme + +@Composable +fun MainScreen(navigateTo: (Route) -> Unit) { + Scaffold { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 24.dp, vertical = 64.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Top section + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_passcodes), + contentDescription = "Passcodes Icon" + ) + + Spacer(Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.displaySmall + ) + + Text( + text = stringResource(R.string.app_version), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Middle actions (primary content) + Card( + modifier = Modifier + .scale(1.25f) + .width(IntrinsicSize.Max), + shape = MaterialTheme.shapes.extraLarge + ) { + Column( + modifier = Modifier.padding(24.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { navigateTo(Route.PasswordManager) }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(imageVector = Icons.Default.Lock, contentDescription = "lock") + Spacer(modifier = Modifier.padding(4.dp)) + Text(text = stringResource(R.string.password_manager_button_text), style = MaterialTheme.typography.bodyLarge) + } + + FilledTonalButton( + onClick = { navigateTo(Route.Settings) }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(imageVector = Icons.Default.Settings, contentDescription = "lock") + Spacer(modifier = Modifier.padding(4.dp)) + Text(stringResource(R.string.setting_button_text), style = MaterialTheme.typography.bodyLarge) + } + + FilledTonalButton( + onClick = { navigateTo(Route.AboutUs) }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(imageVector = Icons.Default.Info, contentDescription = "lock") + Spacer(modifier = Modifier.padding(4.dp)) + Text(stringResource(R.string.about_us_button_text), style = MaterialTheme.typography.bodyLarge) + } + } + } + } + } +} + + +@Preview(showBackground = true) +@PreviewLightDark +@Composable +fun MainScreenPreview() { + PasscodesTheme { + MainScreen(navigateTo = { }) + } +} diff --git a/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SettingsScreen.kt b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SettingsScreen.kt new file mode 100644 index 00000000..7fd41374 --- /dev/null +++ b/app/src/main/kotlin/com/jeeldobariya/passcodes/ui/SettingsScreen.kt @@ -0,0 +1,187 @@ +package com.jeeldobariya.passcodes.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.jeeldobariya.passcodes.R +import com.jeeldobariya.passcodes.core.feature_flags.FeatureFlagsSettings +import com.jeeldobariya.passcodes.core.feature_flags.featureFlagsDatastore +import com.jeeldobariya.passcodes.ui.ui.theme.PasscodesTheme +import kotlinx.coroutines.launch + +@Composable +fun SettingsScreen() { + val scope = rememberCoroutineScope() + val flagDataStore = LocalContext.current.featureFlagsDatastore + val flagDatastoreState by flagDataStore.data.collectAsState( + FeatureFlagsSettings( + version = 0, + isPreviewFeaturesEnabled = false, + isPreviewLayoutEnabled = false + ) + ) + + Scaffold { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(horizontal = 24.dp, vertical = 64.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Top section + Image( + painter = painterResource(R.drawable.ic_passcodes), + contentDescription = "Passcodes Icon", + modifier = Modifier + .size(150.dp) + .padding(10.dp) + ) + + Text( + text = stringResource(R.string.textview_settings_headline), + style = MaterialTheme.typography.titleLarge + ) + + Text( + text = stringResource(R.string.app_version), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(Modifier.height(32.dp)) + + Card { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Coming Soon", style = MaterialTheme.typography.labelLarge) + Text( + text = "Language & Theme features are currently under development", + style = MaterialTheme.typography.labelSmall, + textAlign = TextAlign.Center + ) + } + + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 2.dp) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = stringResource(R.string.preview_layout), style = MaterialTheme.typography.bodyLarge) + + Switch( + checked = flagDatastoreState.isPreviewLayoutEnabled, + onCheckedChange = { + scope.launch { + flagDataStore.updateData { + it.copy(isPreviewLayoutEnabled = !it.isPreviewLayoutEnabled) + } + } + } + ) + } + + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 2.dp) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = stringResource(R.string.latest_feature), style = MaterialTheme.typography.bodyLarge) + + Switch( + checked = flagDatastoreState.isPreviewFeaturesEnabled, + onCheckedChange = { + scope.launch { + flagDataStore.updateData { + it.copy(isPreviewFeaturesEnabled = !it.isPreviewFeaturesEnabled) + } + } + } + ) + } + + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 2.dp) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + OutlinedButton( + modifier = Modifier.align(Alignment.TopStart), + onClick = { } + ) { + Icon(imageVector = Icons.Default.Settings, contentDescription = "settings") + Spacer(modifier = Modifier.padding(2.dp)) + Text(text = stringResource(R.string.autofill_settings)) + } + + OutlinedButton( + modifier = Modifier.align(Alignment.TopEnd), + onClick = { } + ) { + Icon(imageVector = Icons.Default.Delete, contentDescription = "delete", tint = Color.Red) + Spacer(modifier = Modifier.padding(2.dp)) + Text(text = stringResource(R.string.clear_all_data_button_text), color = Color.Red) + } + } + } + } + } +} + + +@Preview(showBackground = true) +@PreviewLightDark +@Composable +fun SettingsScreenPreview() { + PasscodesTheme { + SettingsScreen() + } +} diff --git a/app/src/main/res/drawable/banner.png b/app/src/main/res/drawable/banner.png new file mode 100644 index 00000000..e83be759 Binary files /dev/null and b/app/src/main/res/drawable/banner.png differ diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 6b94fb57..c3177aad 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -49,6 +49,10 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.material) + // Navigation 3 + implementation(libs.bundles.navigation3) + implementation(libs.kotlinx.serialization.json) + // Networking/Parsing implementation(libs.okhttp) implementation(libs.json) diff --git a/core/src/main/kotlin/com/jeeldobariya/passcodes/core/navigation/Route.kt b/core/src/main/kotlin/com/jeeldobariya/passcodes/core/navigation/Route.kt new file mode 100644 index 00000000..335c1c76 --- /dev/null +++ b/core/src/main/kotlin/com/jeeldobariya/passcodes/core/navigation/Route.kt @@ -0,0 +1,23 @@ +package com.jeeldobariya.passcodes.core.navigation + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +sealed interface Route: NavKey { + + @Serializable + data object Home: Route, NavKey + + @Serializable + data object AboutUs: Route, NavKey + + @Serializable + data object Settings: Route, NavKey + + @Serializable + data object PasswordManager: Route, NavKey + + @Serializable + data object SavePassword: Route, NavKey +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9055e090..b7edb6a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,27 +2,29 @@ # Library versions coreKtx = "1.17.0" material = "1.13.0" +nav3Core = "1.0.0" +lifecycleViewmodelNav3 = "2.10.0" okhttp = "5.3.2" oss-license = "17.3.0" appcompat = "1.7.1" room = "2.8.4" -json = "20250517" +json = "20251224" junit = "4.13.2" truth = "1.4.5" androidx-test-ext-junit = "1.3.0" coroutines = "1.10.2" lifecycle = "2.10.0" koin = "4.1.1" -composeBom = "2025.12.00" -compose-activity = "1.12.1" +composeBom = "2025.12.01" +compose-activity = "1.12.2" compose-viewmodel = "2.10.0" datastorePreferences = "1.2.0" kotlinSerializationJson = "1.9.0" # Plugin versions agp = "8.13.2" -ksp = "2.3.3" -kotlin = "2.2.21" +ksp = "2.3.4" +kotlin = "2.3.0" oss-license-plugin = "0.10.10" # Also update in settings.gradle.kts @@ -31,13 +33,20 @@ oss-license-plugin = "0.10.10" # Also update in settings.gradle.kts [libraries] +# Android jetpack compose defualt +# androidx-ui = { module = "androidx.compose.ui:ui" } +# androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } +# androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +# androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } + # Jetpack Compose compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } -compose-ui-material = { module = "androidx.compose.material3:material3" } +compose-ui-material3 = { module = "androidx.compose.material3:material3" } compose-ui-preview = { module = "androidx.compose.ui:ui-tooling-preview" } compose-ui-tooling-debug = { module = "androidx.compose.ui:ui-tooling" } compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" } compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "compose-viewmodel" } +material-icons = { module = "androidx.compose.material:material-icons-core" } # Core Dependency androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } @@ -45,6 +54,11 @@ material = { module = "com.google.android.material:material", version.ref = "mat oss-license = { module = "com.google.android.gms:play-services-oss-licenses", version.ref = "oss-license" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +# Core Navigation 3 libraries +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" } + # DataStore Preference androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerializationJson" } @@ -85,9 +99,12 @@ androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "a [bundles] # Jetpack Compose Dependencies -compose = ["compose-ui-material", "compose-ui-preview", "compose-activity", "compose-viewmodel"] +compose = ["compose-ui-material3", "compose-ui-preview", "compose-activity", "compose-viewmodel", "material-icons"] compose-debug = ["compose-ui-tooling-debug"] +# Navigation 3 +navigation3 = ["androidx-navigation3-runtime", "androidx-navigation3-ui", "androidx-lifecycle-viewmodel-navigation3"] + # Coroutines Dependencies coroutines = ["coroutines-core", "coroutines-android"] diff --git a/password_manager/build.gradle.kts b/password_manager/build.gradle.kts index 70198049..ace0a8fc 100644 --- a/password_manager/build.gradle.kts +++ b/password_manager/build.gradle.kts @@ -3,6 +3,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.jetbrains.kotlin.serialization) } kotlin { @@ -45,6 +47,7 @@ android { buildFeatures { viewBinding = true + compose = true } } @@ -59,6 +62,16 @@ dependencies { // Jetpack Compose implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose) + debugImplementation(libs.bundles.compose.debug) + + // Navigation 3 + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) + implementation(libs.kotlinx.serialization.json) + + // Google Play License Services + implementation(libs.oss.license) // Concurrency (Coroutines Bundle) implementation(libs.coroutines.core) diff --git a/password_manager/src/main/kotlin/com/jeeldobariya/passcodes/password_manager/ui/PasswordManagerScreen.kt b/password_manager/src/main/kotlin/com/jeeldobariya/passcodes/password_manager/ui/PasswordManagerScreen.kt new file mode 100644 index 00000000..8b834cc2 --- /dev/null +++ b/password_manager/src/main/kotlin/com/jeeldobariya/passcodes/password_manager/ui/PasswordManagerScreen.kt @@ -0,0 +1,88 @@ +package com.jeeldobariya.passcodes.password_manager.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.jeeldobariya.passcodes.core.navigation.Route +import com.jeeldobariya.passcodes.password_manager.presentation.load_password.LoadPasswordAction +import com.jeeldobariya.passcodes.password_manager.presentation.load_password.LoadPasswordViewModel +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PasswordManagerScreen(navigateTo: (Route) -> Unit, viewmodel: LoadPasswordViewModel = koinViewModel()) { + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + topBar = { + TopAppBar( + title = { Text("Password Manager") }, + navigationIcon = { + IconButton(onClick = { + scope.launch { + snackbarHostState.showSnackbar("Coming Soon") + } + }) { + Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = "") + } + } + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = { navigateTo(Route.SavePassword) }) { + Icon(imageVector = Icons.Default.AddCircle, contentDescription = "") + } + } + ) { paddingValue -> + viewmodel.onAction(LoadPasswordAction.RefreshPassword) + val state = viewmodel.state.collectAsState() + + LazyColumn(modifier = Modifier.padding(paddingValue)) { + (state.value.passwordEntityList).forEach { + item { + Column( + modifier = Modifier + .padding(12.dp) + .fillMaxSize() + ) { + Text(text = it.domain, style = MaterialTheme.typography.titleLarge) + Text(text = it.username, style = MaterialTheme.typography.bodySmall) + } + + HorizontalDivider(thickness = 2.dp) + } + } + } + } +} + +@PreviewLightDark +@Composable +fun PasswordManagerScreenPreview() { + PasswordManagerScreen({ }) +} diff --git a/password_manager/src/main/kotlin/com/jeeldobariya/passcodes/password_manager/ui/SavePasswordScreen.kt b/password_manager/src/main/kotlin/com/jeeldobariya/passcodes/password_manager/ui/SavePasswordScreen.kt new file mode 100644 index 00000000..25d47c17 --- /dev/null +++ b/password_manager/src/main/kotlin/com/jeeldobariya/passcodes/password_manager/ui/SavePasswordScreen.kt @@ -0,0 +1,102 @@ +package com.jeeldobariya.passcodes.password_manager.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.jeeldobariya.passcodes.password_manager.presentation.save_password.SavePasswordAction +import com.jeeldobariya.passcodes.password_manager.presentation.save_password.SavePasswordViewModel +import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun SavePasswordScreen(viewmodel: SavePasswordViewModel = koinViewModel()) { + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + ) { paddingValues -> + val state by viewmodel.state.collectAsState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + value = state.domain, + onValueChange = { + viewmodel.onAction(action = SavePasswordAction.OnChangeDomain(it)) + }, + label = { + Text("Domain") + } + ) + OutlinedTextField( + value = state.username, + onValueChange = { + viewmodel.onAction(action = SavePasswordAction.OnChangeUsername(it)) + }, + label = { + Text("Username") + } + ) + OutlinedTextField( + value = state.password, + onValueChange = { + viewmodel.onAction(action = SavePasswordAction.OnChangePassword(it)) + }, + label = { + Text("Password") + }, + visualTransformation = PasswordVisualTransformation() + ) + OutlinedTextField( + value = state.notes, + onValueChange = { + viewmodel.onAction(action = SavePasswordAction.OnChangeNotes(it)) + }, + label = { + Text("Notes") + } + ) + + Spacer(modifier = Modifier.padding(4.dp)) + + Button(onClick = { + viewmodel.onAction(action = SavePasswordAction.OnSavePasswordButtonClick) + scope.launch { + snackbarHostState.showSnackbar("Saved Successfully!!") + } + }) { + Text("Save Password") + } + } + } +} + +@PreviewLightDark +@Composable +fun SavePasswordScreenPreview() { + SavePasswordScreen() +}