Merge feature/v1.4.1-bugfixes: Bugfixes + Checklist improvements
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,3 +42,4 @@ Thumbs.db
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
|
test-apks/
|
||||||
|
|||||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.4.1] - 2026-01-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🗑️ Löschen älterer Notizen (v1.2.0 Kompatibilität)**
|
||||||
|
- Notizen aus App-Version v1.2.0 oder früher werden jetzt korrekt vom Server gelöscht
|
||||||
|
- Behebt Problem bei Multi-Device-Nutzung mit älteren Notizen
|
||||||
|
|
||||||
|
- **🔄 Checklisten-Sync Abwärtskompatibilität**
|
||||||
|
- Checklisten werden jetzt auch als Text-Fallback im `content`-Feld gespeichert
|
||||||
|
- Ältere App-Versionen (v1.3.x) zeigen Checklisten als lesbaren Text
|
||||||
|
- Format: GitHub-Style Task-Listen (`[ ] Item` / `[x] Item`)
|
||||||
|
- Recovery-Mode: Falls Checklisten-Items verloren gehen, werden sie aus dem Content wiederhergestellt
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- **📝 Checklisten Auto-Zeilenumbruch**
|
||||||
|
- Lange Checklisten-Texte werden jetzt automatisch umgebrochen
|
||||||
|
- Keine Begrenzung auf 3 Zeilen mehr
|
||||||
|
- Enter-Taste erstellt weiterhin ein neues Item
|
||||||
|
|
||||||
|
### Looking Ahead
|
||||||
|
|
||||||
|
> 🚀 **v1.5.0** wird das nächste größere Release. Wir sammeln Ideen und Feedback!
|
||||||
|
> Feature-Requests gerne als [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues) einreichen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.4.0] - 2026-01-10
|
## [1.4.0] - 2026-01-10
|
||||||
|
|
||||||
### 🎉 New Feature: Checklists
|
### 🎉 New Feature: Checklists
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "dev.dettmer.simplenotes"
|
applicationId = "dev.dettmer.simplenotes"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 11 // 🚀 v1.4.0: Checklists Feature
|
versionCode = 12 // 🔧 v1.4.1: Bugfixes (Root-Delete, Checklist Compat)
|
||||||
versionName = "1.4.0" // 🚀 v1.4.0: Checklists, Multi-Device Sync Fixes, UX Improvements
|
versionName = "1.4.1" // 🔧 v1.4.1: Root-Folder Delete Fix, Checklisten-Sync Abwärtskompatibilität
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupFab()
|
setupFab()
|
||||||
|
|
||||||
|
// v1.4.1: Migrate checklists for backwards compatibility
|
||||||
|
migrateChecklistsForBackwardsCompat()
|
||||||
|
|
||||||
loadNotes()
|
loadNotes()
|
||||||
|
|
||||||
// 🔄 v1.3.1: Observe sync state for UI updates
|
// 🔄 v1.3.1: Observe sync state for UI updates
|
||||||
@@ -730,6 +733,54 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1.4.1: Migriert bestehende Checklisten für Abwärtskompatibilität.
|
||||||
|
*
|
||||||
|
* Problem: v1.4.0 Checklisten haben leeren "content", was auf älteren
|
||||||
|
* App-Versionen (v1.3.x) als leere Notiz angezeigt wird.
|
||||||
|
*
|
||||||
|
* Lösung: Alle Checklisten ohne Fallback-Content als PENDING markieren,
|
||||||
|
* damit sie beim nächsten Sync mit Fallback-Content hochgeladen werden.
|
||||||
|
*
|
||||||
|
* TODO: Diese Migration kann entfernt werden, sobald v1.4.0 nicht mehr
|
||||||
|
* im Umlauf ist (ca. 6 Monate nach v1.4.1 Release, also ~Juli 2026).
|
||||||
|
* Tracking: https://github.com/inventory69/simple-notes-sync/issues/XXX
|
||||||
|
*/
|
||||||
|
private fun migrateChecklistsForBackwardsCompat() {
|
||||||
|
val migrationKey = "v1.4.1_checklist_migration_done"
|
||||||
|
|
||||||
|
// Nur einmal ausführen
|
||||||
|
if (prefs.getBoolean(migrationKey, false)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val allNotes = storage.loadAllNotes()
|
||||||
|
val checklistsToMigrate = allNotes.filter { note ->
|
||||||
|
note.noteType == NoteType.CHECKLIST &&
|
||||||
|
note.content.isBlank() &&
|
||||||
|
note.checklistItems?.isNotEmpty() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checklistsToMigrate.isNotEmpty()) {
|
||||||
|
Logger.d(TAG, "🔄 v1.4.1 Migration: Found ${checklistsToMigrate.size} checklists without fallback content")
|
||||||
|
|
||||||
|
for (note in checklistsToMigrate) {
|
||||||
|
// Als PENDING markieren, damit beim nächsten Sync der Fallback-Content
|
||||||
|
// generiert und hochgeladen wird
|
||||||
|
val updatedNote = note.copy(
|
||||||
|
syncStatus = dev.dettmer.simplenotes.models.SyncStatus.PENDING
|
||||||
|
)
|
||||||
|
storage.saveNote(updatedNote)
|
||||||
|
Logger.d(TAG, " 📝 Marked for re-sync: ${note.title}")
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.d(TAG, "✅ v1.4.1 Migration: ${checklistsToMigrate.size} checklists marked for re-sync")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration als erledigt markieren
|
||||||
|
prefs.edit().putBoolean(migrationKey, true).apply()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
requestCode: Int,
|
requestCode: Int,
|
||||||
permissions: Array<out String>,
|
permissions: Array<out String>,
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ package dev.dettmer.simplenotes.adapters
|
|||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -55,33 +53,31 @@ class ChecklistEditorAdapter(
|
|||||||
editText.setText(item.text)
|
editText.setText(item.text)
|
||||||
updateStrikethrough(item.isChecked)
|
updateStrikethrough(item.isChecked)
|
||||||
|
|
||||||
// TextWatcher für Änderungen
|
// v1.4.1: TextWatcher für Änderungen + Enter-Erkennung für neues Item
|
||||||
textWatcher = object : TextWatcher {
|
textWatcher = object : TextWatcher {
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
val pos = bindingAdapterPosition
|
val pos = bindingAdapterPosition
|
||||||
if (pos != RecyclerView.NO_POSITION) {
|
if (pos == RecyclerView.NO_POSITION) return
|
||||||
onItemTextChanged(pos, s?.toString() ?: "")
|
|
||||||
|
val text = s?.toString() ?: ""
|
||||||
|
|
||||||
|
// Prüfe ob ein Newline eingegeben wurde
|
||||||
|
if (text.contains("\n")) {
|
||||||
|
// Newline entfernen und neues Item erstellen
|
||||||
|
val cleanText = text.replace("\n", "")
|
||||||
|
editText.setText(cleanText)
|
||||||
|
editText.setSelection(cleanText.length)
|
||||||
|
onItemTextChanged(pos, cleanText)
|
||||||
|
onAddNewItem(pos + 1)
|
||||||
|
} else {
|
||||||
|
onItemTextChanged(pos, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editText.addTextChangedListener(textWatcher)
|
editText.addTextChangedListener(textWatcher)
|
||||||
|
|
||||||
// Enter-Taste = neues Item
|
|
||||||
editText.setOnEditorActionListener { _, actionId, event ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_NEXT ||
|
|
||||||
(event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
|
|
||||||
val pos = bindingAdapterPosition
|
|
||||||
if (pos != RecyclerView.NO_POSITION) {
|
|
||||||
onAddNewItem(pos + 1)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete Button
|
// Delete Button
|
||||||
deleteButton.setOnClickListener {
|
deleteButton.setOnClickListener {
|
||||||
val pos = bindingAdapterPosition
|
val pos = bindingAdapterPosition
|
||||||
|
|||||||
@@ -20,13 +20,42 @@ data class Note(
|
|||||||
val checklistItems: List<ChecklistItem>? = null
|
val checklistItems: List<ChecklistItem>? = null
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Serialisiert Note zu JSON (v1.4.0: Nutzt Gson für komplexe Strukturen)
|
* Serialisiert Note zu JSON
|
||||||
|
* v1.4.0: Nutzt Gson für komplexe Strukturen
|
||||||
|
* v1.4.1: Für Checklisten wird ein Fallback-Content generiert, damit ältere
|
||||||
|
* App-Versionen (v1.3.x) die Notiz als Text anzeigen können.
|
||||||
*/
|
*/
|
||||||
fun toJson(): String {
|
fun toJson(): String {
|
||||||
val gson = com.google.gson.GsonBuilder()
|
val gson = com.google.gson.GsonBuilder()
|
||||||
.setPrettyPrinting()
|
.setPrettyPrinting()
|
||||||
.create()
|
.create()
|
||||||
return gson.toJson(this)
|
|
||||||
|
// v1.4.1: Für Checklisten den Fallback-Content generieren
|
||||||
|
val noteToSerialize = if (noteType == NoteType.CHECKLIST && checklistItems != null) {
|
||||||
|
this.copy(content = generateChecklistFallbackContent())
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
return gson.toJson(noteToSerialize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1.4.1: Generiert einen lesbaren Text-Fallback aus Checklist-Items.
|
||||||
|
* Format: GitHub-Style Task-Listen (kompatibel mit Markdown)
|
||||||
|
*
|
||||||
|
* Beispiel:
|
||||||
|
* [ ] Milch kaufen
|
||||||
|
* [x] Brot gekauft
|
||||||
|
* [ ] Eier
|
||||||
|
*
|
||||||
|
* Wird von älteren App-Versionen (v1.3.x) als normaler Text angezeigt.
|
||||||
|
*/
|
||||||
|
private fun generateChecklistFallbackContent(): String {
|
||||||
|
return checklistItems?.sortedBy { it.order }?.joinToString("\n") { item ->
|
||||||
|
val checkbox = if (item.isChecked) "[x]" else "[ ]"
|
||||||
|
"$checkbox ${item.text}"
|
||||||
|
} ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +117,7 @@ type: ${noteType.name.lowercase()}
|
|||||||
|
|
||||||
// Checklist-Items parsen (kann null sein)
|
// Checklist-Items parsen (kann null sein)
|
||||||
val checklistItemsType = object : com.google.gson.reflect.TypeToken<List<ChecklistItem>>() {}.type
|
val checklistItemsType = object : com.google.gson.reflect.TypeToken<List<ChecklistItem>>() {}.type
|
||||||
val checklistItems = if (jsonObject.has("checklistItems") &&
|
var checklistItems: List<ChecklistItem>? = if (jsonObject.has("checklistItems") &&
|
||||||
!jsonObject.get("checklistItems").isJsonNull
|
!jsonObject.get("checklistItems").isJsonNull
|
||||||
) {
|
) {
|
||||||
gson.fromJson<List<ChecklistItem>>(
|
gson.fromJson<List<ChecklistItem>>(
|
||||||
@@ -99,6 +128,19 @@ type: ${noteType.name.lowercase()}
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.4.1: Recovery-Mode - Falls Checkliste aber keine Items,
|
||||||
|
// versuche Content als Fallback zu parsen
|
||||||
|
if (noteType == NoteType.CHECKLIST &&
|
||||||
|
(checklistItems == null || checklistItems.isEmpty()) &&
|
||||||
|
rawNote.content.isNotBlank()) {
|
||||||
|
|
||||||
|
val recoveredItems = parseChecklistFromContent(rawNote.content)
|
||||||
|
if (recoveredItems.isNotEmpty()) {
|
||||||
|
Logger.d(TAG, "🔄 Recovered ${recoveredItems.size} checklist items from content fallback")
|
||||||
|
checklistItems = recoveredItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note mit korrekten Werten erstellen
|
// Note mit korrekten Werten erstellen
|
||||||
Note(
|
Note(
|
||||||
id = rawNote.id,
|
id = rawNote.id,
|
||||||
@@ -130,6 +172,34 @@ type: ${noteType.name.lowercase()}
|
|||||||
val syncStatus: SyncStatus? = null
|
val syncStatus: SyncStatus? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1.4.1: Parst GitHub-Style Checklisten aus Text (Recovery-Mode).
|
||||||
|
*
|
||||||
|
* Unterstützte Formate:
|
||||||
|
* - [ ] Unchecked item
|
||||||
|
* - [x] Checked item
|
||||||
|
* - [X] Checked item (case insensitive)
|
||||||
|
*
|
||||||
|
* Wird verwendet, wenn eine v1.4.0 Checkliste von einer älteren
|
||||||
|
* App-Version (v1.3.x) bearbeitet wurde und die checklistItems verloren gingen.
|
||||||
|
*
|
||||||
|
* @param content Der Text-Content der Notiz
|
||||||
|
* @return Liste von ChecklistItems oder leere Liste
|
||||||
|
*/
|
||||||
|
private fun parseChecklistFromContent(content: String): List<ChecklistItem> {
|
||||||
|
val pattern = Regex("""^\s*\[([ xX])\]\s*(.+)$""", RegexOption.MULTILINE)
|
||||||
|
return pattern.findAll(content).mapIndexed { index, match ->
|
||||||
|
val checked = match.groupValues[1].lowercase() == "x"
|
||||||
|
val text = match.groupValues[2].trim()
|
||||||
|
ChecklistItem(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
text = text,
|
||||||
|
isChecked = checked,
|
||||||
|
order = index
|
||||||
|
)
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parst Markdown zurück zu Note-Objekt (Task #1.2.0-09)
|
* Parst Markdown zurück zu Note-Objekt (Task #1.2.0-09)
|
||||||
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
|
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
|
||||||
|
|||||||
@@ -1764,6 +1764,9 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
* Deletes a note from the server (JSON + Markdown)
|
* Deletes a note from the server (JSON + Markdown)
|
||||||
* Does NOT delete from local storage!
|
* Does NOT delete from local storage!
|
||||||
*
|
*
|
||||||
|
* v1.4.1: Now supports v1.2.0 compatibility mode - also checks ROOT folder
|
||||||
|
* for notes that were created before the /notes/ directory structure.
|
||||||
|
*
|
||||||
* @param noteId The ID of the note to delete
|
* @param noteId The ID of the note to delete
|
||||||
* @return true if at least one file was deleted, false otherwise
|
* @return true if at least one file was deleted, false otherwise
|
||||||
*/
|
*/
|
||||||
@@ -1775,12 +1778,21 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
var deletedJson = false
|
var deletedJson = false
|
||||||
var deletedMd = false
|
var deletedMd = false
|
||||||
|
|
||||||
// Delete JSON
|
// v1.4.1: Try to delete JSON from /notes/ first (standard path)
|
||||||
val jsonUrl = getNotesUrl(serverUrl) + "$noteId.json"
|
val jsonUrl = getNotesUrl(serverUrl) + "$noteId.json"
|
||||||
if (sardine.exists(jsonUrl)) {
|
if (sardine.exists(jsonUrl)) {
|
||||||
sardine.delete(jsonUrl)
|
sardine.delete(jsonUrl)
|
||||||
deletedJson = true
|
deletedJson = true
|
||||||
Logger.d(TAG, "🗑️ Deleted from server: $noteId.json")
|
Logger.d(TAG, "🗑️ Deleted from server: $noteId.json (from /notes/)")
|
||||||
|
} else {
|
||||||
|
// v1.4.1: Fallback - check ROOT folder for v1.2.0 compatibility
|
||||||
|
val rootJsonUrl = serverUrl.trimEnd('/') + "/$noteId.json"
|
||||||
|
Logger.d(TAG, "🔍 JSON not in /notes/, checking ROOT: $rootJsonUrl")
|
||||||
|
if (sardine.exists(rootJsonUrl)) {
|
||||||
|
sardine.delete(rootJsonUrl)
|
||||||
|
deletedJson = true
|
||||||
|
Logger.d(TAG, "🗑️ Deleted from server: $noteId.json (from ROOT - v1.2.0 compat)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete Markdown (v1.3.0: YAML-scan based approach)
|
// Delete Markdown (v1.3.0: YAML-scan based approach)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
android:minHeight="0dp" />
|
android:minHeight="0dp" />
|
||||||
|
|
||||||
<!-- Text Input (ohne Box, nur transparent) -->
|
<!-- Text Input (ohne Box, nur transparent) -->
|
||||||
|
<!-- v1.4.1: Auto-Zeilenumbruch für lange Texte -->
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/etItemText"
|
android:id="@+id/etItemText"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@@ -38,9 +39,7 @@
|
|||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:hint="@string/item_placeholder"
|
android:hint="@string/item_placeholder"
|
||||||
android:inputType="text"
|
android:inputType="textMultiLine|textCapSentences"
|
||||||
android:imeOptions="actionNext"
|
|
||||||
android:maxLines="3"
|
|
||||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||||
tools:text="Milch kaufen" />
|
tools:text="Milch kaufen" />
|
||||||
|
|
||||||
|
|||||||
4
fastlane/metadata/android/de-DE/changelogs/12.txt
Normal file
4
fastlane/metadata/android/de-DE/changelogs/12.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
• Bugfix: Löschen von Notizen aus älteren App-Versionen (v1.2.0)
|
||||||
|
• Bugfix: Checklisten-Sync mit älteren App-Versionen (v1.3.x)
|
||||||
|
• Checklisten werden jetzt auch als Text-Fallback gespeichert
|
||||||
|
• Neu: Checklisten-Texte werden automatisch umgebrochen
|
||||||
5
fastlane/metadata/android/en-US/changelogs/12.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/12.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
• Bugfix: Deleting notes from older app versions (v1.2.0)
|
||||||
|
• Bugfix: Checklist sync with older app versions (v1.3.x)
|
||||||
|
• Checklists are now also saved as text fallback
|
||||||
|
• New: Checklist texts now wrap automatically
|
||||||
|
p automatically
|
||||||
@@ -147,7 +147,21 @@ Builds:
|
|||||||
scandelete:
|
scandelete:
|
||||||
- android/gradle/wrapper
|
- android/gradle/wrapper
|
||||||
|
|
||||||
|
- versionName: 1.4.1
|
||||||
|
versionCode: 12
|
||||||
|
commit: v1.4.1
|
||||||
|
subdir: android/app
|
||||||
|
sudo:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y openjdk-17-jdk-headless
|
||||||
|
- update-java-alternatives -a
|
||||||
|
gradle:
|
||||||
|
- fdroid
|
||||||
|
prebuild: sed -i -e '/signingConfig/d' build.gradle.kts
|
||||||
|
scandelete:
|
||||||
|
- android/gradle/wrapper
|
||||||
|
|
||||||
AutoUpdateMode: Version
|
AutoUpdateMode: Version
|
||||||
UpdateCheckMode: Tags
|
UpdateCheckMode: Tags
|
||||||
CurrentVersion: 1.4.0
|
CurrentVersion: 1.4.1
|
||||||
CurrentVersionCode: 11
|
CurrentVersionCode: 12
|
||||||
|
|||||||
Reference in New Issue
Block a user