diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index dd045cb..d7252dd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -33,15 +33,32 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.10"
+ }
}
dependencies {
-
implementation("androidx.core:core-ktx:1.12.0")
- implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
- implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+
+ val composeBom = platform("androidx.compose:compose-bom:2024.02.01")
+ implementation(composeBom)
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
-}
\ No newline at end of file
+ androidTestImplementation(composeBom)
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+}
diff --git a/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt b/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt
index 4ec83e7..89955f0 100644
--- a/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt
+++ b/app/src/main/java/cz/bugsy/passwordzebra/MainActivity.kt
@@ -3,88 +3,296 @@ package cz.bugsy.passwordzebra
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
+import android.content.res.Configuration
import android.os.Bundle
-import android.widget.Button
-import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
-
-private const val KEY_PASSWORD = "password"
-
-class MainActivity : AppCompatActivity() {
-
- private lateinit var generateButton: Button
- private lateinit var passwordTextView: TextView
- private lateinit var switchWithoutSpaces: com.google.android.material.switchmaterial.SwitchMaterial
- private lateinit var switchSpecialChars: com.google.android.material.switchmaterial.SwitchMaterial
- private lateinit var passwordLengthPicker: android.widget.NumberPicker
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+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.fillMaxHeight
+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.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.FilledIconButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+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.LocalConfiguration
+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.unit.dp
+import androidx.compose.ui.unit.sp
+import cz.bugsy.passwordzebra.ui.components.NumberPickerView
+import cz.bugsy.passwordzebra.ui.theme.PasswordZebraTheme
+class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
-
- generateButton = findViewById(R.id.generateButton)
- passwordTextView = findViewById(R.id.passwordTextView)
- switchWithoutSpaces = findViewById(R.id.switchWithoutSpaces)
- switchSpecialChars = findViewById(R.id.switchSpecialChars)
- passwordLengthPicker = findViewById(R.id.passwordLengthPicker)
-
- // Set the range of selectable values for password length (1 to 10)
- val defaultPasswordLength = 4
- passwordLengthPicker.minValue = 1
- passwordLengthPicker.maxValue = 10
- passwordLengthPicker.value = defaultPasswordLength
-
- generateButton.setOnClickListener {
- generatePassword()
- }
-
- // Restore password after rotation, otherwise generate a new one
- if (savedInstanceState != null) {
- passwordTextView.text = savedInstanceState.getString(KEY_PASSWORD, "")
- } else {
- generatePassword()
- }
-
- // Set click listener to copy password to clipboard
- passwordTextView.setOnClickListener {
- copyToClipboard(passwordTextView.text.toString(), getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- outState.putString(KEY_PASSWORD, passwordTextView.text.toString())
- }
-
- private fun generatePassword() {
- val passwordLength = passwordLengthPicker.value
- val password = generateRandomWords(passwordLength)
- passwordTextView.text = password
- }
-
- private fun generateRandomWords(numWords: Int): String {
- return buildString {
- repeat(numWords) {
- append(PasswordGenerator.generateRandomWord())
- append(" ") // Add space between words
+ enableEdgeToEdge()
+ setContent {
+ PasswordZebraTheme {
+ PasswordZebraApp()
}
- }.let { password ->
- // Check if the switch for adding special characters is checked
- if (switchSpecialChars.isChecked) {
- PasswordGenerator.addSpecialCharacters(password)
- } else {
- password
- }
- }.trim() // Remove trailing space
- }
-
- private fun copyToClipboard(text: String, clipboardManager: ClipboardManager) {
- val clipData = if (switchWithoutSpaces.isChecked) {
- val textNoSpaces = text.filter { !it.isWhitespace() }
- ClipData.newPlainText("Password", textNoSpaces)
- } else {
- ClipData.newPlainText("Password", text)
}
- clipboardManager.setPrimaryClip(clipData)
}
}
+
+@Composable
+private fun PasswordZebraApp() {
+ var password by rememberSaveable { mutableStateOf("") }
+ var wordCount by rememberSaveable { mutableIntStateOf(4) }
+ var withoutSpaces by rememberSaveable { mutableStateOf(false) }
+ var addSpecialChars by rememberSaveable { mutableStateOf(false) }
+
+ val context = LocalContext.current
+
+ LaunchedEffect(Unit) {
+ if (password.isEmpty()) {
+ password = generatePassword(wordCount, addSpecialChars)
+ }
+ }
+
+ val onGenerate = { password = generatePassword(wordCount, addSpecialChars) }
+ val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
+
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ if (isLandscape) {
+ // Landscape: controls left (vertically centered), password + generate button right
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ // Left: controls only, vertically centered
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ ControlsPanel(
+ wordCount = wordCount,
+ withoutSpaces = withoutSpaces,
+ addSpecialChars = addSpecialChars,
+ onWordCountChange = { wordCount = it },
+ onWithoutSpacesChange = { withoutSpaces = it },
+ onAddSpecialCharsChange = { addSpecialChars = it },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ // Right: password box fills remaining height, generate button pinned to bottom
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ PasswordBox(
+ password = password,
+ onClick = { copyToClipboard(context, password, withoutSpaces) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ GenerateButton(onClick = onGenerate)
+ }
+ }
+ } else {
+ // Portrait: password box top, controls middle, generate button pinned to bottom
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.weight(1f))
+ PasswordBox(
+ password = password,
+ onClick = { copyToClipboard(context, password, withoutSpaces) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(150.dp),
+ )
+ Spacer(modifier = Modifier.weight(2f))
+ ControlsPanel(
+ wordCount = wordCount,
+ withoutSpaces = withoutSpaces,
+ addSpecialChars = addSpecialChars,
+ onWordCountChange = { wordCount = it },
+ onWithoutSpacesChange = { withoutSpaces = it },
+ onAddSpecialCharsChange = { addSpecialChars = it },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ Spacer(modifier = Modifier.weight(3f))
+ GenerateButton(onClick = onGenerate)
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+ }
+ }
+}
+
+@Composable
+private fun PasswordBox(password: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .border(
+ width = 2.dp,
+ color = MaterialTheme.colorScheme.primary,
+ shape = RoundedCornerShape(12.dp),
+ )
+ .clickable { onClick() }
+ .padding(16.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = password,
+ fontSize = 24.sp,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+}
+
+@Composable
+private fun ControlsPanel(
+ wordCount: Int,
+ withoutSpaces: Boolean,
+ addSpecialChars: Boolean,
+ onWordCountChange: (Int) -> Unit,
+ onWithoutSpacesChange: (Boolean) -> Unit,
+ onAddSpecialCharsChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.pw_straighten),
+ contentDescription = stringResource(R.string.image_length_desc),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.textview_password_length),
+ modifier = Modifier.weight(1f),
+ )
+ NumberPickerView(
+ value = wordCount,
+ minValue = 1,
+ maxValue = 10,
+ onValueChange = onWordCountChange,
+ modifier = Modifier.widthIn(min = 80.dp),
+ )
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.pw_letter_spacing),
+ contentDescription = stringResource(R.string.image_wospaces),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.switch_without_spaces),
+ modifier = Modifier.weight(1f),
+ )
+ Switch(
+ checked = withoutSpaces,
+ onCheckedChange = onWithoutSpacesChange,
+ )
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.pw_special_character),
+ contentDescription = stringResource(R.string.image_specialchars),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = stringResource(R.string.switch_special_characters),
+ modifier = Modifier.weight(1f),
+ )
+ Switch(
+ checked = addSpecialChars,
+ onCheckedChange = onAddSpecialCharsChange,
+ )
+ }
+ }
+}
+
+@Composable
+private fun GenerateButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
+ FilledIconButton(
+ onClick = onClick,
+ modifier = modifier.size(60.dp),
+ shape = RoundedCornerShape(16.dp),
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.pw_generate),
+ contentDescription = stringResource(R.string.button_generate_description),
+ )
+ }
+}
+
+private fun generatePassword(wordCount: Int, addSpecialChars: Boolean): String {
+ return buildString {
+ repeat(wordCount) {
+ append(PasswordGenerator.generateRandomWord())
+ append(" ")
+ }
+ }.let { password ->
+ if (addSpecialChars) {
+ PasswordGenerator.addSpecialCharacters(password)
+ } else {
+ password
+ }
+ }.trim()
+}
+
+private fun copyToClipboard(context: Context, text: String, withoutSpaces: Boolean) {
+ val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clipText = if (withoutSpaces) text.filter { !it.isWhitespace() } else text
+ val clipData = ClipData.newPlainText("Password", clipText)
+ clipboardManager.setPrimaryClip(clipData)
+}
diff --git a/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt b/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt
index a5d6b7b..767bf90 100644
--- a/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt
+++ b/app/src/main/java/cz/bugsy/passwordzebra/PasswordZebra.kt
@@ -2,16 +2,10 @@ package cz.bugsy.passwordzebra
import android.app.Application
import com.google.android.material.color.DynamicColors
-import com.google.android.material.color.DynamicColorsOptions
class PasswordZebra : Application() {
override fun onCreate() {
- // Apply dynamic color
- DynamicColors.applyToActivitiesIfAvailable(
- this,
- DynamicColorsOptions.Builder()
- .setThemeOverlay(R.style.AppTheme_Overlay)
- .build()
- )
+ super.onCreate()
+ DynamicColors.applyToActivitiesIfAvailable(this)
}
}
diff --git a/app/src/main/java/cz/bugsy/passwordzebra/ui/components/NumberPickerView.kt b/app/src/main/java/cz/bugsy/passwordzebra/ui/components/NumberPickerView.kt
new file mode 100644
index 0000000..ced2681
--- /dev/null
+++ b/app/src/main/java/cz/bugsy/passwordzebra/ui/components/NumberPickerView.kt
@@ -0,0 +1,30 @@
+package cz.bugsy.passwordzebra.ui.components
+
+import android.widget.NumberPicker
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+
+@Composable
+fun NumberPickerView(
+ value: Int,
+ minValue: Int,
+ maxValue: Int,
+ onValueChange: (Int) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ AndroidView(
+ factory = { ctx ->
+ NumberPicker(ctx).apply {
+ this.minValue = minValue
+ this.maxValue = maxValue
+ this.value = value
+ setOnValueChangedListener { _, _, new -> onValueChange(new) }
+ }
+ },
+ update = { picker ->
+ if (picker.value != value) picker.value = value
+ },
+ modifier = modifier,
+ )
+}
diff --git a/app/src/main/java/cz/bugsy/passwordzebra/ui/theme/Theme.kt b/app/src/main/java/cz/bugsy/passwordzebra/ui/theme/Theme.kt
new file mode 100644
index 0000000..28cb7ab
--- /dev/null
+++ b/app/src/main/java/cz/bugsy/passwordzebra/ui/theme/Theme.kt
@@ -0,0 +1,92 @@
+package cz.bugsy.passwordzebra.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+
+private val LightColorScheme = lightColorScheme(
+ primary = Color(0xFF286C2A),
+ onPrimary = Color(0xFFFFFFFF),
+ primaryContainer = Color(0xFFABF5A3),
+ onPrimaryContainer = Color(0xFF002203),
+ secondary = Color(0xFF00639A),
+ onSecondary = Color(0xFFFFFFFF),
+ secondaryContainer = Color(0xFFCEE5FF),
+ onSecondaryContainer = Color(0xFF001D32),
+ tertiary = Color(0xFF38656A),
+ onTertiary = Color(0xFFFFFFFF),
+ tertiaryContainer = Color(0xFFBCEBF0),
+ onTertiaryContainer = Color(0xFF002023),
+ error = Color(0xFFBA1A1A),
+ errorContainer = Color(0xFFFFDAD6),
+ onError = Color(0xFFFFFFFF),
+ onErrorContainer = Color(0xFF410002),
+ background = Color(0xFFFCFDF6),
+ onBackground = Color(0xFF1A1C19),
+ surface = Color(0xFFFCFDF6),
+ onSurface = Color(0xFF1A1C19),
+ surfaceVariant = Color(0xFFDEE5D8),
+ onSurfaceVariant = Color(0xFF424940),
+ outline = Color(0xFF72796F),
+ inverseOnSurface = Color(0xFFF1F1EB),
+ inverseSurface = Color(0xFF2F312D),
+ inversePrimary = Color(0xFF90D889),
+ surfaceTint = Color(0xFF286C2A),
+ outlineVariant = Color(0xFFC2C9BD),
+ scrim = Color(0xFF000000),
+)
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Color(0xFF90D889),
+ onPrimary = Color(0xFF003909),
+ primaryContainer = Color(0xFF085314),
+ onPrimaryContainer = Color(0xFFABF5A3),
+ secondary = Color(0xFF96CCFF),
+ onSecondary = Color(0xFF003353),
+ secondaryContainer = Color(0xFF004A75),
+ onSecondaryContainer = Color(0xFFCEE5FF),
+ tertiary = Color(0xFFA0CFD4),
+ onTertiary = Color(0xFF00363B),
+ tertiaryContainer = Color(0xFF1E4D52),
+ onTertiaryContainer = Color(0xFFBCEBF0),
+ error = Color(0xFFFFB4AB),
+ errorContainer = Color(0xFF93000A),
+ onError = Color(0xFF690005),
+ onErrorContainer = Color(0xFFFFDAD6),
+ background = Color(0xFF1A1C19),
+ onBackground = Color(0xFFE2E3DD),
+ surface = Color(0xFF1A1C19),
+ onSurface = Color(0xFFE2E3DD),
+ surfaceVariant = Color(0xFF424940),
+ onSurfaceVariant = Color(0xFFC2C9BD),
+ outline = Color(0xFF8C9388),
+ inverseOnSurface = Color(0xFF1A1C19),
+ inverseSurface = Color(0xFFE2E3DD),
+ inversePrimary = Color(0xFF286C2A),
+ surfaceTint = Color(0xFF90D889),
+ outlineVariant = Color(0xFF424940),
+ scrim = Color(0xFF000000),
+)
+
+@Composable
+fun PasswordZebraTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val colorScheme = when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val ctx = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx)
+ }
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ MaterialTheme(colorScheme = colorScheme, content = content)
+}
diff --git a/app/src/main/res/drawable/password_background.xml b/app/src/main/res/drawable/password_background.xml
deleted file mode 100644
index 3369cc4..0000000
--- a/app/src/main/res/drawable/password_background.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml
deleted file mode 100644
index 3556290..0000000
--- a/app/src/main/res/layout-land/activity_main.xml
+++ /dev/null
@@ -1,167 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 4de68df..0000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,161 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index c1bac58..e09dff1 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,18 +1,12 @@
-
-
-
-
-
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index e174afc..58d00ac 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,8 +1,6 @@
-
-
-
-
diff --git a/app/src/main/res/values/themes_overlays.xml b/app/src/main/res/values/themes_overlays.xml
index 1f8c2ea..aea9118 100644
--- a/app/src/main/res/values/themes_overlays.xml
+++ b/app/src/main/res/values/themes_overlays.xml
@@ -1,6 +1,3 @@
-
-
-
\ No newline at end of file
+
+
+