feat: Add guided tour feature
This commit introduces a guided tour feature to the app. - A new has been added to provide an introduction to the app's functionalities. - A "Show Guided Tour" button has been added to the settings screen, allowing users to revisit the tour. - Logic has been implemented to display the guided tour automatically on the first launch of the app. - String resources for the guided tour have been added in both English and German. - Accompanist Pager dependencies have been added to for the tour's UI. - Minor fixes to XML escaping in string resources have been applied.
This commit is contained in:
@@ -66,6 +66,7 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.ui.text) // Added for KeyboardOptions
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.reorderable)
|
||||
implementation(libs.androidx.material.icons.extended)
|
||||
@@ -73,6 +74,9 @@ dependencies {
|
||||
implementation(libs.google.material)
|
||||
implementation(libs.compose.markdown)
|
||||
|
||||
// Accompanist Pager
|
||||
implementation("com.google.accompanist:accompanist-pager:0.32.0")
|
||||
implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0")
|
||||
|
||||
// Room Database
|
||||
implementation(libs.androidx.room.runtime)
|
||||
|
||||
@@ -121,6 +121,7 @@ import de.lxtools.noteshop.security.FileEncryptor
|
||||
import android.util.Log
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import de.lxtools.noteshop.ui.EncryptionPasswordDialog
|
||||
import de.lxtools.noteshop.ui.GuidedTourScreen
|
||||
import java.io.FileOutputStream
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@@ -298,6 +299,7 @@ sealed class Screen(val route: String, val titleRes: Int) : Parcelable {
|
||||
data object About : Screen("about", R.string.menu_about)
|
||||
data object Settings : Screen("settings", R.string.menu_settings)
|
||||
data object WebAppIntegration : Screen("webapp_integration", R.string.webapp_integration_title)
|
||||
data object GuidedTour : Screen("guided_tour", R.string.show_guided_tour)
|
||||
}
|
||||
|
||||
// Function to get DocumentFile for a given URI and filename
|
||||
@@ -768,6 +770,14 @@ fun AppShell(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val firstLaunchCompleted = sharedPrefs.getBoolean("first_launch_completed", false)
|
||||
if (!firstLaunchCompleted) {
|
||||
currentScreen = Screen.GuidedTour
|
||||
sharedPrefs.edit { putBoolean("first_launch_completed", true) }
|
||||
}
|
||||
}
|
||||
var selectedListId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var selectedNoteId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var selectedRecipeId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
@@ -1893,10 +1903,39 @@ fun AppShell(
|
||||
)
|
||||
}
|
||||
is Screen.WebAppIntegration -> {
|
||||
val webAppIntegrationViewModel: de.lxtools.noteshop.ui.webapp.WebAppIntegrationViewModel = viewModel(factory = AppViewModelProvider.Factory)
|
||||
val onAuthenticateAndTogglePasswordVisibility: (Boolean) -> Unit = { isPassword ->
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.authenticate_to_perform_action),
|
||||
subtitle = "",
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
onSuccess = {
|
||||
if (isPassword) {
|
||||
webAppIntegrationViewModel.togglePasswordVisibility()
|
||||
} else {
|
||||
webAppIntegrationViewModel.toggleDeletePasswordVisibility()
|
||||
}
|
||||
},
|
||||
onFailed = {
|
||||
android.widget.Toast.makeText(context, R.string.unlock_failed, android.widget.Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onError = { _, _ ->
|
||||
android.widget.Toast.makeText(context, R.string.unlock_failed, android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
de.lxtools.noteshop.ui.webapp.WebAppIntegrationScreen(
|
||||
viewModel = viewModel(factory = AppViewModelProvider.Factory),
|
||||
viewModel = webAppIntegrationViewModel,
|
||||
onNavigateUp = { currentScreen = Screen.Settings },
|
||||
padding = innerPadding
|
||||
padding = innerPadding,
|
||||
onAuthenticateAndTogglePasswordVisibility = onAuthenticateAndTogglePasswordVisibility
|
||||
)
|
||||
}
|
||||
is Screen.GuidedTour -> {
|
||||
de.lxtools.noteshop.ui.GuidedTourScreen(
|
||||
onTourFinished = { currentScreen = Screen.Settings }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
105
app/src/main/java/de/lxtools/noteshop/ui/GuidedTourScreen.kt
Normal file
105
app/src/main/java/de/lxtools/noteshop/ui/GuidedTourScreen.kt
Normal file
@@ -0,0 +1,105 @@
|
||||
package de.lxtools.noteshop.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.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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 com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.HorizontalPagerIndicator
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import de.lxtools.noteshop.R
|
||||
|
||||
@Composable
|
||||
fun GuidedTourScreen(onTourFinished: () -> Unit) {
|
||||
val pagerState = rememberPagerState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
HorizontalPager(
|
||||
count = 3,
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f)
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> TourPage(
|
||||
imageRes = R.drawable.noteshop_logo,
|
||||
title = stringResource(id = R.string.tour_page_1_title),
|
||||
text = stringResource(id = R.string.tour_page_1_text)
|
||||
)
|
||||
1 -> TourPage(
|
||||
imageRes = R.drawable.noteshop_logo,
|
||||
title = stringResource(id = R.string.tour_page_2_title),
|
||||
text = stringResource(id = R.string.tour_page_2_text)
|
||||
)
|
||||
2 -> TourPage(
|
||||
imageRes = R.drawable.noteshop_logo,
|
||||
title = stringResource(id = R.string.tour_page_3_title),
|
||||
text = stringResource(id = R.string.tour_page_3_text)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPagerIndicator(
|
||||
pagerState = pagerState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(16.dp)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onTourFinished,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(text = if (pagerState.currentPage == 2) "Fertig" else "Überspringen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TourPage(imageRes: Int, title: String, text: String) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = imageRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(200.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,14 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
|
||||
item {
|
||||
Button(onClick = { onNavigate(Screen.GuidedTour) }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.show_guided_tour))
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||
item { HorizontalDivider() }
|
||||
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||
|
||||
@@ -4,6 +4,8 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -15,6 +17,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.lxtools.noteshop.R
|
||||
|
||||
@@ -23,8 +26,11 @@ import de.lxtools.noteshop.R
|
||||
fun WebAppIntegrationScreen(
|
||||
viewModel: WebAppIntegrationViewModel,
|
||||
onNavigateUp: () -> Unit,
|
||||
padding: PaddingValues
|
||||
padding: PaddingValues,
|
||||
onAuthenticateAndTogglePasswordVisibility: (Boolean) -> Unit // New parameter
|
||||
) {
|
||||
var showResetConfirmationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
@@ -50,14 +56,34 @@ fun WebAppIntegrationScreen(
|
||||
value = viewModel.password,
|
||||
onValueChange = viewModel::onPasswordChange,
|
||||
label = { Text(stringResource(R.string.password_label)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
visualTransformation = if (viewModel.showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
val image = if (viewModel.showPassword)
|
||||
Icons.Filled.Visibility
|
||||
else Icons.Filled.VisibilityOff
|
||||
val description = if (viewModel.showPassword) "Hide password" else "Show password"
|
||||
|
||||
IconButton(onClick = { onAuthenticateAndTogglePasswordVisibility(true) }) {
|
||||
Icon(imageVector = image, contentDescription = description)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = viewModel.deletePassword,
|
||||
onValueChange = viewModel::onDeletePasswordChange,
|
||||
label = { Text(stringResource(R.string.delete_password_label)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
visualTransformation = if (viewModel.showDeletePassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
val image = if (viewModel.showDeletePassword)
|
||||
Icons.Filled.Visibility
|
||||
else Icons.Filled.VisibilityOff
|
||||
val description = if (viewModel.showDeletePassword) "Hide password" else "Show password"
|
||||
|
||||
IconButton(onClick = { onAuthenticateAndTogglePasswordVisibility(false) }) {
|
||||
Icon(imageVector = image, contentDescription = description)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
@@ -70,8 +96,6 @@ fun WebAppIntegrationScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp)) // Add some space between buttons
|
||||
|
||||
var showResetConfirmationDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
onClick = { showResetConfirmationDialog = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -103,4 +127,4 @@ fun WebAppIntegrationScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import de.lxtools.noteshop.data.NoteshopRepository
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import de.lxtools.noteshop.security.KeyManager
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class WebAppIntegrationViewModel(private val repository: NoteshopRepository, application: Application) : AndroidViewModel(application) {
|
||||
|
||||
@@ -30,10 +31,36 @@ class WebAppIntegrationViewModel(private val repository: NoteshopRepository, app
|
||||
var deletePassword by mutableStateOf("")
|
||||
private set
|
||||
|
||||
private val _showPassword = mutableStateOf(false)
|
||||
val showPassword: Boolean by _showPassword
|
||||
|
||||
private val _showDeletePassword = mutableStateOf(false)
|
||||
val showDeletePassword: Boolean by _showDeletePassword
|
||||
|
||||
init {
|
||||
loadCredentials()
|
||||
}
|
||||
|
||||
fun togglePasswordVisibility() {
|
||||
_showPassword.value = !_showPassword.value
|
||||
if (_showPassword.value) {
|
||||
viewModelScope.launch {
|
||||
kotlinx.coroutines.delay(10000) // Hide after 10 seconds
|
||||
_showPassword.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleDeletePasswordVisibility() {
|
||||
_showDeletePassword.value = !_showDeletePassword.value
|
||||
if (_showDeletePassword.value) {
|
||||
viewModelScope.launch {
|
||||
kotlinx.coroutines.delay(10000) // Hide after 10 seconds
|
||||
_showDeletePassword.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onWebAppUrlChange(newUrl: String) {
|
||||
webAppUrl = newUrl
|
||||
}
|
||||
|
||||
@@ -302,4 +302,12 @@
|
||||
<string name="web_app_integration_reset_successful">Web-App-Integration erfolgreich zurückgesetzt</string>
|
||||
<string name="failed_to_decrypt_credentials">Fehler beim Entschlüsseln der Zugangsdaten</string>
|
||||
<string name="deletion_password_cannot_be_empty">Löschpasswort darf nicht leer sein</string>
|
||||
|
||||
<string name="tour_page_1_title">Willkommen bei Noteshop!</string>
|
||||
<string name="tour_page_1_text">Verwalte deine Notizen, Einkaufslisten und Rezepte an einem Ort.</string>
|
||||
<string name="tour_page_2_title">Alles organisieren</string>
|
||||
<string name="tour_page_2_text">Erstelle, bearbeite und kategorisiere deine Notizen und Listen mit Leichtigkeit.</string>
|
||||
<string name="tour_page_3_title">Sicher & Privat</string>
|
||||
<string name="tour_page_3_text">Deine Daten gehören dir. Verschlüssele deine Daten für maximale Privatsphäre.</string>
|
||||
<string name="show_guided_tour">Geführte Tour anzeigen</string>
|
||||
</resources>
|
||||
@@ -302,4 +302,12 @@
|
||||
<string name="web_app_integration_reset_successful">Web App Integration reset successfully</string>
|
||||
<string name="failed_to_decrypt_credentials">Failed to decrypt credentials</string>
|
||||
<string name="deletion_password_cannot_be_empty">Deletion password cannot be empty</string>
|
||||
|
||||
<string name="tour_page_1_title">Welcome to Noteshop!</string>
|
||||
<string name="tour_page_1_text">Manage your notes, shopping lists, and recipes all in one place.</string>
|
||||
<string name="tour_page_2_title">Organize Everything</string>
|
||||
<string name="tour_page_2_text">Create, edit, and categorize your notes and lists with ease.</string>
|
||||
<string name="tour_page_3_title">Secure & Private</string>
|
||||
<string name="tour_page_3_text">Your data is yours. Encrypt your data for maximum privacy.</string>
|
||||
<string name="show_guided_tour">Show Guided Tour</string>
|
||||
</resources>
|
||||
@@ -36,6 +36,7 @@ androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
|
||||
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||
androidx-compose-ui-text = { module = "androidx.compose.ui:ui-text" } # Added for KeyboardOptions
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
|
||||
# Room database libraries
|
||||
|
||||
Reference in New Issue
Block a user