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:
inventory69
2026-02-10 16:38:39 +01:00
parent 3e946edafb
commit 661d9e0992
11 changed files with 317 additions and 18 deletions

1
android/.gitignore vendored
View File

@@ -18,3 +18,4 @@ local.properties
key.properties key.properties
*.jks *.jks
*.keystore *.keystore
/app/src/main/assets/changelogs/

View File

@@ -200,3 +200,33 @@ detekt {
// Parallel-Verarbeitung für schnellere Checks // Parallel-Verarbeitung für schnellere Checks
parallel = true 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")
}

View File

@@ -170,6 +170,9 @@ class ComposeMainActivity : ComponentActivity() {
onOpenSettings = { openSettings() }, onOpenSettings = { openSettings() },
onCreateNote = { noteType -> createNote(noteType) } onCreateNote = { noteType -> createNote(noteType) }
) )
// v1.8.0: Post-Update Changelog (shows once after update)
UpdateChangelogSheet()
} }
} }
} }

View File

@@ -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}"
}
}
}

View File

@@ -811,6 +811,16 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
fun getLogFile() = Logger.getLogFile(getApplication()) 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 // Helper
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════

View File

@@ -119,6 +119,31 @@ fun DebugSettingsScreen(
) )
Spacer(modifier = Modifier.height(16.dp)) 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))
} }
} }

View File

@@ -79,4 +79,7 @@ object Constants {
const val KEY_SORT_DIRECTION = "sort_direction" const val KEY_SORT_DIRECTION = "sort_direction"
const val DEFAULT_SORT_OPTION = "updatedAt" const val DEFAULT_SORT_OPTION = "updatedAt"
const val DEFAULT_SORT_DIRECTION = "desc" const val DEFAULT_SORT_DIRECTION = "desc"
// 📋 v1.8.0: Post-Update Changelog
const val KEY_LAST_SHOWN_CHANGELOG_VERSION = "last_shown_changelog_version"
} }

View File

@@ -403,6 +403,10 @@
<string name="debug_delete_logs">🗑️ Logs löschen</string> <string name="debug_delete_logs">🗑️ Logs löschen</string>
<string name="debug_delete_logs_title">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_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 --> <!-- 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> <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_title">Changelog</string>
<string name="about_changelog_subtitle">Vollständige Versionshistorie</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 --> <!-- v1.8.0: Backup Progress -->
<string name="backup_progress_creating">Backup wird erstellt…</string> <string name="backup_progress_creating">Backup wird erstellt…</string>
<string name="backup_progress_restoring">Backup wird wiederhergestellt…</string> <string name="backup_progress_restoring">Backup wird wiederhergestellt…</string>

View File

@@ -403,6 +403,10 @@
<string name="debug_delete_logs">🗑️ Delete Logs</string> <string name="debug_delete_logs">🗑️ Delete Logs</string>
<string name="debug_delete_logs_title">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_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 --> <!-- 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> <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_title">Changelog</string>
<string name="about_changelog_subtitle">Full version history</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 --> <!-- v1.8.0: Backup Progress -->
<string name="backup_progress_creating">Creating backup…</string> <string name="backup_progress_creating">Creating backup…</string>
<string name="backup_progress_restoring">Restoring backup…</string> <string name="backup_progress_restoring">Restoring backup…</string>

View File

@@ -1,12 +1,11 @@
🎉 v1.8.0 — WIDGETS, SORTIERUNG & SYNC-VERBESSERUNGEN 🎉 v1.8.0 — WIDGETS & UI-VERBESSERUNGEN
• Neu: Homescreen-Widgets mit interaktiven Checkboxen • Neu: Homescreen-Widgets mit interaktiven Checkboxen
• Neu: Widget-Transparenz & Sperr-Einstellungen
• Neu: Notiz-Sortierung (Datum, Titel, Typ) • Neu: Notiz-Sortierung (Datum, Titel, Typ)
• Neu: Checklisten Auto-Sort (Offen zuerst/Erledigt zuletzt) • Neu: Parallele Downloads (1-10 gleichzeitig)
Neu: Server-Löschungs-Erkennung für Multi-Device-Sync Verbessert: Raster-Standard, Sync-Struktur, Live-Fortschritt
Neu: Sync-Status-Legende (Hilfe erklärt alle Icons) Weitere UI/UX-Verbesserungen
• Verbessert: Live Sync-Fortschritt mit Phasen-Anzeige
• Verbessert: Parallele Downloads für schnelleren Sync Vollständiger Changelog:
• Verbessert: Checklisten-Überlauf-Verlauf bei langem Text https://github.com/inventory69/simple-notes-sync/blob/main/CHANGELOG.de.md
• Behoben: Drag & Drop Flackern in Checklisten
• Behoben: Widget-Textnotizen Scrolling

View File

@@ -1,12 +1,13 @@
🎉 v1.8.0 — WIDGETS, SORTING & SYNC IMPROVEMENTS 🎉 v1.8.0 — WIDGETS & UI POLISH
• New: Home screen widgets with interactive checkboxes • New: Home screen widgets with interactive checkboxes
• New: Widget opacity & lock settings
• New: Note sorting (date, title, type) • New: Note sorting (date, title, type)
• New: Checklist auto-sort (unchecked first/checked last) • New: Parallel downloads (1-10 simultaneous)
New: Server deletion detection for multi-device sync Improved: Grid view as default layout
New: Sync status legend (help button explains all icons) Improved: Sync settings reorganized into clear sections
• Improved: Live sync progress with phase indicators • Improved: Live sync progress with status indicators
Improved: Parallel downloads for faster sync More UI/UX improvements
• Improved: Checklist overflow gradient for long text
• Fixed: Drag & drop flicker in checklists Full changelog:
• Fixed: Widget text notes scrolling https://github.com/inventory69/simple-notes-sync/blob/main/CHANGELOG.md