feat(v1.8.0): IMPL_06 Post-Update Changelog Dialog
- Add UpdateChangelogSheet.kt with Material 3 ModalBottomSheet
- Show changelog automatically on first launch after update
- Load changelog from F-Droid metadata via assets (single source of truth)
- Add copyChangelogsToAssets Gradle task (runs before preBuild)
- Copy F-Droid changelogs to /assets/changelogs/{locale}/ at build time
- Store last shown version in SharedPreferences (last_shown_changelog_version)
- Add ClickableText for GitHub CHANGELOG.md link (opens in browser)
- Add update_changelog_title and update_changelog_dismiss strings (EN + DE)
- Add KEY_LAST_SHOWN_CHANGELOG_VERSION constant
- Integrate UpdateChangelogSheet in ComposeMainActivity
- Add Test Mode in Debug Settings with "Reset Changelog Dialog" button
- Add SettingsViewModel.resetChangelogVersion() for testing
- Add test mode strings (debug_test_section, debug_reset_changelog, etc.)
- Update F-Droid changelogs (20.txt) with focus on key features
- Add exception logging in loadChangelog() function
- Add /app/src/main/assets/changelogs/ to .gitignore
- Dialog dismissable via button or swipe gesture
- One-time display per versionCode
Adds post-update changelog dialog with automatic F-Droid changelog reuse.
F-Droid changelogs are the single source of truth for both F-Droid metadata
and in-app display. Gradle task copies changelogs to assets at build time.
Users see localized changelog (DE/EN) based on app language.
This commit is contained in:
1
android/.gitignore
vendored
1
android/.gitignore
vendored
@@ -18,3 +18,4 @@ local.properties
|
||||
key.properties
|
||||
*.jks
|
||||
*.keystore
|
||||
/app/src/main/assets/changelogs/
|
||||
|
||||
@@ -199,4 +199,34 @@ detekt {
|
||||
|
||||
// Parallel-Verarbeitung für schnellere Checks
|
||||
parallel = true
|
||||
}
|
||||
|
||||
// 📋 v1.8.0: Copy F-Droid changelogs to assets for post-update dialog
|
||||
// Single source of truth: F-Droid changelogs are reused in the app
|
||||
tasks.register<Copy>("copyChangelogsToAssets") {
|
||||
description = "Copies F-Droid changelogs to app assets for post-update dialog"
|
||||
|
||||
from("$rootDir/../fastlane/metadata/android") {
|
||||
include("*/changelogs/*.txt")
|
||||
}
|
||||
|
||||
into("$projectDir/src/main/assets/changelogs")
|
||||
|
||||
// Preserve directory structure: en-US/20.txt, de-DE/20.txt
|
||||
eachFile {
|
||||
val parts = relativePath.segments
|
||||
if (parts.size >= 3) {
|
||||
// parts[0] = locale (en-US, de-DE)
|
||||
// parts[1] = "changelogs"
|
||||
// parts[2] = version file (20.txt)
|
||||
relativePath = RelativePath(true, parts[0], parts[2])
|
||||
}
|
||||
}
|
||||
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
|
||||
// Run before preBuild to ensure changelogs are available
|
||||
tasks.named("preBuild") {
|
||||
dependsOn("copyChangelogsToAssets")
|
||||
}
|
||||
@@ -170,6 +170,9 @@ class ComposeMainActivity : ComponentActivity() {
|
||||
onOpenSettings = { openSettings() },
|
||||
onCreateNote = { noteType -> createNote(noteType) }
|
||||
)
|
||||
|
||||
// v1.8.0: Post-Update Changelog (shows once after update)
|
||||
UpdateChangelogSheet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
package dev.dettmer.simplenotes.ui.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.dettmer.simplenotes.BuildConfig
|
||||
import dev.dettmer.simplenotes.R
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* v1.8.0: Post-Update Changelog Bottom Sheet
|
||||
*
|
||||
* Shows a subtle changelog on first launch after an update.
|
||||
* - Reads changelog from raw resources (supports DE/EN)
|
||||
* - Only shows once per versionCode (stored in SharedPreferences)
|
||||
* - Uses Material 3 ModalBottomSheet with built-in slide-up animation
|
||||
* - Dismissable via button or swipe-down
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UpdateChangelogSheet() {
|
||||
val context = LocalContext.current
|
||||
val prefs = remember {
|
||||
context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
val currentVersionCode = BuildConfig.VERSION_CODE
|
||||
val lastShownVersion = prefs.getInt(Constants.KEY_LAST_SHOWN_CHANGELOG_VERSION, 0)
|
||||
|
||||
// Only show if this is a new version
|
||||
var showSheet by remember { mutableStateOf(currentVersionCode > lastShownVersion) }
|
||||
|
||||
if (!showSheet) return
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Load changelog text based on current locale
|
||||
val changelogText = remember {
|
||||
loadChangelog(context)
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
showSheet = false
|
||||
prefs.edit()
|
||||
.putInt(Constants.KEY_LAST_SHOWN_CHANGELOG_VERSION, currentVersionCode)
|
||||
.apply()
|
||||
},
|
||||
sheetState = sheetState,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
tonalElevation = 2.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.navigationBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Title
|
||||
Text(
|
||||
text = stringResource(R.string.update_changelog_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Changelog content with clickable links
|
||||
val annotatedText = buildAnnotatedString {
|
||||
val lines = changelogText.split("\n")
|
||||
lines.forEachIndexed { index, line ->
|
||||
if (line.startsWith("http://") || line.startsWith("https://")) {
|
||||
// Make URLs clickable
|
||||
pushStringAnnotation(
|
||||
tag = "URL",
|
||||
annotation = line.trim()
|
||||
)
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
) {
|
||||
append(line)
|
||||
}
|
||||
pop()
|
||||
} else {
|
||||
append(line)
|
||||
}
|
||||
if (index < lines.size - 1) append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedText,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { offset ->
|
||||
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
|
||||
.firstOrNull()?.let { annotation ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(annotation.item))
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Dismiss button
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
sheetState.hide()
|
||||
}.invokeOnCompletion {
|
||||
showSheet = false
|
||||
prefs.edit()
|
||||
.putInt(Constants.KEY_LAST_SHOWN_CHANGELOG_VERSION, currentVersionCode)
|
||||
.apply()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.update_changelog_dismiss))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load changelog text from assets based on current app locale and versionCode.
|
||||
* Changelogs are copied from /fastlane/metadata/android/{locale}/changelogs/{versionCode}.txt
|
||||
* at build time, providing a single source of truth for F-Droid and in-app display.
|
||||
* Falls back to English if the localized version is not available.
|
||||
*/
|
||||
private fun loadChangelog(context: Context): String {
|
||||
val currentLocale = AppCompatDelegate.getApplicationLocales()
|
||||
val languageCode = if (currentLocale.isEmpty) {
|
||||
// System default — check system locale
|
||||
java.util.Locale.getDefault().language
|
||||
} else {
|
||||
currentLocale.get(0)?.language ?: "en"
|
||||
}
|
||||
|
||||
// Map language code to F-Droid locale directory
|
||||
val localeDir = when (languageCode) {
|
||||
"de" -> "de-DE"
|
||||
else -> "en-US"
|
||||
}
|
||||
|
||||
val versionCode = BuildConfig.VERSION_CODE
|
||||
val changelogPath = "changelogs/$localeDir/$versionCode.txt"
|
||||
|
||||
return try {
|
||||
context.assets.open(changelogPath)
|
||||
.bufferedReader()
|
||||
.use { it.readText() }
|
||||
} catch (e: Exception) {
|
||||
Logger.e("UpdateChangelogSheet", "Failed to load changelog for locale: $localeDir", e)
|
||||
// Fallback to English
|
||||
try {
|
||||
context.assets.open("changelogs/en-US/$versionCode.txt")
|
||||
.bufferedReader()
|
||||
.use { it.readText() }
|
||||
} catch (e2: Exception) {
|
||||
Logger.e("UpdateChangelogSheet", "Failed to load English fallback changelog", e2)
|
||||
"v${BuildConfig.VERSION_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -811,6 +811,16 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
||||
|
||||
fun getLogFile() = Logger.getLogFile(getApplication())
|
||||
|
||||
/**
|
||||
* v1.8.0: Reset changelog version to force showing the changelog dialog on next start
|
||||
* Used for testing the post-update changelog feature
|
||||
*/
|
||||
fun resetChangelogVersion() {
|
||||
prefs.edit()
|
||||
.putInt(Constants.KEY_LAST_SHOWN_CHANGELOG_VERSION, 0)
|
||||
.apply()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// Helper
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -119,6 +119,31 @@ fun DebugSettingsScreen(
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SettingsDivider()
|
||||
|
||||
// v1.8.0: Test Mode Section
|
||||
SettingsSectionHeader(text = stringResource(R.string.debug_test_section))
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Info about test mode
|
||||
SettingsInfoCard(
|
||||
text = stringResource(R.string.debug_reset_changelog_desc)
|
||||
)
|
||||
|
||||
val changelogResetToast = stringResource(R.string.debug_changelog_reset)
|
||||
|
||||
SettingsButton(
|
||||
text = stringResource(R.string.debug_reset_changelog),
|
||||
onClick = {
|
||||
viewModel.resetChangelogVersion()
|
||||
android.widget.Toast.makeText(context, changelogResetToast, android.widget.Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,4 +79,7 @@ object Constants {
|
||||
const val KEY_SORT_DIRECTION = "sort_direction"
|
||||
const val DEFAULT_SORT_OPTION = "updatedAt"
|
||||
const val DEFAULT_SORT_DIRECTION = "desc"
|
||||
|
||||
// 📋 v1.8.0: Post-Update Changelog
|
||||
const val KEY_LAST_SHOWN_CHANGELOG_VERSION = "last_shown_changelog_version"
|
||||
}
|
||||
|
||||
@@ -403,6 +403,10 @@
|
||||
<string name="debug_delete_logs">🗑️ Logs löschen</string>
|
||||
<string name="debug_delete_logs_title">Logs löschen?</string>
|
||||
<string name="debug_delete_logs_message">Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.</string>
|
||||
<string name="debug_test_section">Test-Modus</string>
|
||||
<string name="debug_reset_changelog">Changelog-Dialog zurücksetzen</string>
|
||||
<string name="debug_reset_changelog_desc">Changelog beim nächsten App-Start anzeigen</string>
|
||||
<string name="debug_changelog_reset">Changelog wird beim nächsten Start angezeigt</string>
|
||||
<!-- Legacy -->
|
||||
<string name="file_logging_privacy_notice">ℹ️ Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren.</string>
|
||||
|
||||
@@ -443,6 +447,10 @@
|
||||
<string name="about_changelog_title">Changelog</string>
|
||||
<string name="about_changelog_subtitle">Vollständige Versionshistorie</string>
|
||||
|
||||
<!-- v1.8.0: Post-Update Changelog Dialog -->
|
||||
<string name="update_changelog_title">Was ist neu</string>
|
||||
<string name="update_changelog_dismiss">Alles klar!</string>
|
||||
|
||||
<!-- v1.8.0: Backup Progress -->
|
||||
<string name="backup_progress_creating">Backup wird erstellt…</string>
|
||||
<string name="backup_progress_restoring">Backup wird wiederhergestellt…</string>
|
||||
|
||||
@@ -403,6 +403,10 @@
|
||||
<string name="debug_delete_logs">🗑️ Delete Logs</string>
|
||||
<string name="debug_delete_logs_title">Delete logs?</string>
|
||||
<string name="debug_delete_logs_message">All saved sync logs will be permanently deleted.</string>
|
||||
<string name="debug_test_section">Test Mode</string>
|
||||
<string name="debug_reset_changelog">Reset Changelog Dialog</string>
|
||||
<string name="debug_reset_changelog_desc">Show changelog on next app start</string>
|
||||
<string name="debug_changelog_reset">Changelog will show on next start</string>
|
||||
<!-- Legacy -->
|
||||
<string name="file_logging_privacy_notice">ℹ️ Privacy: Logs are only stored locally on your device and are never sent to external servers. Logs contain sync activities for troubleshooting. You can delete or export them at any time.</string>
|
||||
|
||||
@@ -443,6 +447,10 @@
|
||||
<string name="about_changelog_title">Changelog</string>
|
||||
<string name="about_changelog_subtitle">Full version history</string>
|
||||
|
||||
<!-- v1.8.0: Post-Update Changelog Dialog -->
|
||||
<string name="update_changelog_title">What\'s New</string>
|
||||
<string name="update_changelog_dismiss">Got it!</string>
|
||||
|
||||
<!-- v1.8.0: Backup Progress -->
|
||||
<string name="backup_progress_creating">Creating backup…</string>
|
||||
<string name="backup_progress_restoring">Restoring backup…</string>
|
||||
|
||||
Reference in New Issue
Block a user