From 661d9e099242ae9ee55869cd129667fdeac8a566 Mon Sep 17 00:00:00 2001 From: inventory69 Date: Tue, 10 Feb 2026 16:38:39 +0100 Subject: [PATCH] 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. --- android/.gitignore | 1 + android/app/build.gradle.kts | 30 +++ .../ui/main/ComposeMainActivity.kt | 3 + .../ui/main/UpdateChangelogSheet.kt | 211 ++++++++++++++++++ .../ui/settings/SettingsViewModel.kt | 10 + .../settings/screens/DebugSettingsScreen.kt | 25 +++ .../dettmer/simplenotes/utils/Constants.kt | 3 + .../app/src/main/res/values-de/strings.xml | 8 + android/app/src/main/res/values/strings.xml | 8 + .../metadata/android/de-DE/changelogs/20.txt | 17 +- .../metadata/android/en-US/changelogs/20.txt | 19 +- 11 files changed, 317 insertions(+), 18 deletions(-) create mode 100644 android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt diff --git a/android/.gitignore b/android/.gitignore index bfe4c24..0b21239 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -18,3 +18,4 @@ local.properties key.properties *.jks *.keystore +/app/src/main/assets/changelogs/ diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a8a6293..117f9d4 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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("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") } \ No newline at end of file diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt index e00b4e1..3ba829c 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt @@ -170,6 +170,9 @@ class ComposeMainActivity : ComponentActivity() { onOpenSettings = { openSettings() }, onCreateNote = { noteType -> createNote(noteType) } ) + + // v1.8.0: Post-Update Changelog (shows once after update) + UpdateChangelogSheet() } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt new file mode 100644 index 0000000..0912b96 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt @@ -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}" + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt index 670d755..58e4217 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt @@ -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 // ═══════════════════════════════════════════════════════════════════════ diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt index 99ab408..600ba4b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt @@ -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)) } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt index fb9fed6..bbd94f2 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt @@ -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" } diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index 8ace8fb..4ca3b2a 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -403,6 +403,10 @@ 🗑️ Logs löschen Logs löschen? Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht. + Test-Modus + Changelog-Dialog zurücksetzen + Changelog beim nächsten App-Start anzeigen + Changelog wird beim nächsten Start angezeigt ℹ️ 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. @@ -443,6 +447,10 @@ Changelog Vollständige Versionshistorie + + Was ist neu + Alles klar! + Backup wird erstellt… Backup wird wiederhergestellt… diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 3f406bc..fa42f2b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -403,6 +403,10 @@ 🗑️ Delete Logs Delete logs? All saved sync logs will be permanently deleted. + Test Mode + Reset Changelog Dialog + Show changelog on next app start + Changelog will show on next start ℹ️ 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. @@ -443,6 +447,10 @@ Changelog Full version history + + What\'s New + Got it! + Creating backup… Restoring backup… diff --git a/fastlane/metadata/android/de-DE/changelogs/20.txt b/fastlane/metadata/android/de-DE/changelogs/20.txt index 858c0fe..5c69d4f 100644 --- a/fastlane/metadata/android/de-DE/changelogs/20.txt +++ b/fastlane/metadata/android/de-DE/changelogs/20.txt @@ -1,12 +1,11 @@ -🎉 v1.8.0 — WIDGETS, SORTIERUNG & SYNC-VERBESSERUNGEN +🎉 v1.8.0 — WIDGETS & UI-VERBESSERUNGEN • Neu: Homescreen-Widgets mit interaktiven Checkboxen +• Neu: Widget-Transparenz & Sperr-Einstellungen • Neu: Notiz-Sortierung (Datum, Titel, Typ) -• Neu: Checklisten Auto-Sort (Offen zuerst/Erledigt zuletzt) -• Neu: Server-Löschungs-Erkennung für Multi-Device-Sync -• Neu: Sync-Status-Legende (Hilfe erklärt alle Icons) -• Verbessert: Live Sync-Fortschritt mit Phasen-Anzeige -• Verbessert: Parallele Downloads für schnelleren Sync -• Verbessert: Checklisten-Überlauf-Verlauf bei langem Text -• Behoben: Drag & Drop Flackern in Checklisten -• Behoben: Widget-Textnotizen Scrolling +• Neu: Parallele Downloads (1-10 gleichzeitig) +• Verbessert: Raster-Standard, Sync-Struktur, Live-Fortschritt +• Weitere UI/UX-Verbesserungen + +Vollständiger Changelog: +https://github.com/inventory69/simple-notes-sync/blob/main/CHANGELOG.de.md diff --git a/fastlane/metadata/android/en-US/changelogs/20.txt b/fastlane/metadata/android/en-US/changelogs/20.txt index 5397e05..faac6f1 100644 --- a/fastlane/metadata/android/en-US/changelogs/20.txt +++ b/fastlane/metadata/android/en-US/changelogs/20.txt @@ -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: Widget opacity & lock settings • New: Note sorting (date, title, type) -• New: Checklist auto-sort (unchecked first/checked last) -• New: Server deletion detection for multi-device sync -• New: Sync status legend (help button explains all icons) -• Improved: Live sync progress with phase indicators -• Improved: Parallel downloads for faster sync -• Improved: Checklist overflow gradient for long text -• Fixed: Drag & drop flicker in checklists -• Fixed: Widget text notes scrolling +• New: Parallel downloads (1-10 simultaneous) +• Improved: Grid view as default layout +• Improved: Sync settings reorganized into clear sections +• Improved: Live sync progress with status indicators +• More UI/UX improvements + +Full changelog: +https://github.com/inventory69/simple-notes-sync/blob/main/CHANGELOG.md