From 4ccded41af8f39939c2356639a5c423dd26d3a52 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 20 Feb 2026 11:12:59 +0100 Subject: [PATCH 01/26] [fix] Adds catch-all around note preview to prevent app-start-crashes. The user is then capable of saving the content in the stored format to try to rescue the content. --- .../ui/adapter/NoteAdapter.kt | 37 ++++++++++++++--- .../ui/main/MainActivity.kt | 41 +++++++++++++++++++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt index 02443ac..903f3ca 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt @@ -16,6 +16,7 @@ package org.secuso.privacyfriendlynotes.ui.adapter import android.app.Activity import android.graphics.Color import android.text.Html +import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.View @@ -23,6 +24,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -48,6 +50,7 @@ class NoteAdapter( var notes: MutableList = ArrayList() private set + var saveContent: ((Note, NoteHolder) -> Unit)? = null private var listener: ((Note, NoteHolder) -> Unit)? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteHolder { val itemView = LayoutInflater.from(parent.context) @@ -102,6 +105,7 @@ class NoteAdapter( } } + try { when (currentNote.type) { DbContract.NoteEntry.TYPE_TEXT -> { if (showPreview) { @@ -120,12 +124,23 @@ class NoteAdapter( if (showPreview) { holder.imageViewcategory.setBackgroundColor(run { val value = TypedValue() - holder.itemView.context.theme.resolveAttribute(R.attr.colorSurfaceVariantLight, value, true) + holder.itemView.context.theme.resolveAttribute( + R.attr.colorSurfaceVariantLight, + value, + true + ) value.data }) - holder.imageViewcategory.minimumHeight = 200; holder.imageViewcategory.minimumWidth = 200 - Glide.with(activity).load(File("${activity.application.filesDir.path}/sketches${currentNote.content}")) - .placeholder(AppCompatResources.getDrawable(activity, R.drawable.ic_photo_icon_24dp)) + holder.imageViewcategory.minimumHeight = + 200; holder.imageViewcategory.minimumWidth = 200 + Glide.with(activity) + .load(File("${activity.application.filesDir.path}/sketches${currentNote.content}")) + .placeholder( + AppCompatResources.getDrawable( + activity, + R.drawable.ic_photo_icon_24dp + ) + ) .into(holder.imageViewcategory) } else { holder.imageViewcategory.setImageResource(R.drawable.ic_photo_icon_24dp) @@ -138,14 +153,24 @@ class NoteAdapter( if (showPreview) { val preview = mainActivityViewModel.checklistPreview(currentNote) - holder.textViewExtraText.text = "${preview.filter { it.first }.count()}/${preview.size}" - holder.textViewDescription.text = preview.take(3).joinToString(System.lineSeparator()) { it.second } + holder.textViewExtraText.text = + "${preview.filter { it.first }.count()}/${preview.size}" + holder.textViewDescription.text = + preview.take(3).joinToString(System.lineSeparator()) { it.second } holder.textViewDescription.maxLines = 3 } else { holder.textViewExtraText.text = "-/-" } } } + } catch (error: Exception) { + Log.d("NoteAdapter", "could not preview note.") + error.printStackTrace() + holder.textViewDescription.text = ContextCompat.getString(activity, R.string.preview_note_failed) + holder.itemView.setOnClickListener { + saveContent?.let { it(currentNote, holder) } + } + } // if the Description is empty, don't show it if (holder.textViewDescription.text.toString().isEmpty()) { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt index abf0e6b..49259c1 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -19,6 +19,7 @@ import android.content.Intent import android.graphics.Rect import android.os.Bundle import android.preference.PreferenceManager +import android.text.Html import android.util.Log import android.util.TypedValue import android.view.ContextThemeWrapper @@ -35,6 +36,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.arch.core.util.Function @@ -47,6 +49,7 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationView import kotlinx.coroutines.CoroutineScope @@ -71,6 +74,7 @@ import org.secuso.privacyfriendlynotes.ui.notes.BaseNoteActivity import org.secuso.privacyfriendlynotes.ui.notes.ChecklistNoteActivity import org.secuso.privacyfriendlynotes.ui.notes.SketchActivity import org.secuso.privacyfriendlynotes.ui.notes.TextNoteActivity +import java.io.File import java.io.FileOutputStream import java.io.OutputStream import java.util.Collections @@ -129,6 +133,35 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } } + private var noteToExport: Note? = null + private val saveSingleNoteToExternalStorageResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { uri -> + val fileOutputStream = contentResolver.openOutputStream(uri) + if (fileOutputStream == null) { + return@registerForActivityResult + } + CoroutineScope(Dispatchers.IO).launch { + val content = when (noteToExport?.type) { + DbContract.NoteEntry.TYPE_TEXT -> noteToExport!!.content + DbContract.NoteEntry.TYPE_AUDIO -> File(filesDir.path + "/audio_notes" + noteToExport!!.content).readBytes().toString() + DbContract.NoteEntry.TYPE_SKETCH -> File(filesDir.path + "/sketches" + noteToExport!!.content).readBytes().toString() + DbContract.NoteEntry.TYPE_CHECKLIST -> noteToExport!!.content + else -> return@launch + } + fileOutputStream.bufferedWriter().write(content) + runOnUiThread { + Toast.makeText( + applicationContext, + String.format(getString(R.string.toast_file_exported_to), uri.toString()), + Toast.LENGTH_LONG + ).show() + } + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { supportFragmentManager.fragmentFactory = object : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { @@ -176,6 +209,14 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte PreferenceManager.getDefaultSharedPreferences(this).getBoolean("settings_color_category", true) && mainActivityViewModel.getCategory() == CAT_ALL ) + adapter.saveContent = { note, _ -> + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.putExtra(Intent.EXTRA_TITLE, note.name + ".txt") + intent.type = "text/plain" + noteToExport = note + saveSingleNoteToExternalStorageResultLauncher.launch(intent) + } recyclerView.adapter = adapter lifecycleScope.launch { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0e77a10..2a14fd8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -172,4 +172,5 @@ Dateigrößenlimit für importierte Textnotizen Alle abwählen Sperren + Diese Notiz konnte nicht geladen werden. Wenn Sie versuchen diese Notiz zu öffnen, wird stattdessen versucht den Inhalt der Notiz unformattiert zu speichern. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58042f1..b0c0f90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,4 +196,5 @@ Text Note Character limit for imported text notes File Size limit for imported text notes + This note could not be loaded. Click to save the stored content. From 13613eddb3dd0629d054fcc2330e5aedcbf539e2 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 10:39:03 +0100 Subject: [PATCH 02/26] [fix] zip-export of notes with same name could lead to crash due to name collision. [feat] adds categories to the zip-exported hierarchy, so {category_name}/{note_type}/{note_name}_{note_id}.{ext} --- .../privacyfriendlynotes/room/dao/CategoryDao.kt | 3 +++ .../ui/main/MainActivityViewModel.kt | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt index f9734fa..ec76334 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt @@ -44,6 +44,9 @@ interface CategoryDao { @get:Query("SELECT * FROM categories GROUP BY name") val allCategories: Flow> + @get:Query("SELECT * FROM categories GROUP BY name") + val allCategoriesSync: List + @Query("SELECT name FROM categories WHERE _id=:thisCategoryId ") fun categoryNameFromId(thisCategoryId: Integer): LiveData diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt index 64fce3c..3f8d115 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt @@ -262,25 +262,28 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica fun zipAllNotes(notes: List, output: OutputStream) { ZipOutputStream(output).use { zipOut -> + val categories = + repository.categoryDao().allCategoriesSync.associate { it._id to it.name }.toMutableMap() + categories[CAT_ALL] = "default" notes.forEach { note -> val name = note.name.replace("/", "_") lateinit var entry: String lateinit var inputStream: InputStream when(note.type) { DbContract.NoteEntry.TYPE_TEXT -> { - entry = "text/" + name + "_" + System.currentTimeMillis() + "_" + TextNoteActivity.getFileExtension() + entry = categories[note.category] + "/text/" + name + "_" + note._id + "_" + TextNoteActivity.getFileExtension() inputStream = ByteArrayInputStream(note.content.toByteArray()) } DbContract.NoteEntry.TYPE_CHECKLIST -> { - entry = "checklist/" + name + "_" + System.currentTimeMillis() + "_" + ChecklistNoteActivity.getFileExtension() + entry = categories[note.category] + "/checklist/" + name + "_" + note._id + "_" + ChecklistNoteActivity.getFileExtension() inputStream = ByteArrayInputStream(note.content.toByteArray()) } DbContract.NoteEntry.TYPE_AUDIO -> { - entry = "audio/" + name + "_" + System.currentTimeMillis() + "_" + AudioNoteActivity.getFileExtension() + entry = categories[note.category] + "/audio/" + name + "_" + note._id + "_" + AudioNoteActivity.getFileExtension() inputStream = FileInputStream(File(filesDir.path + "/audio_notes" + note.content)) } DbContract.NoteEntry.TYPE_SKETCH -> { - entry ="sketch/" + name + "_" + System.currentTimeMillis() + "_" + SketchActivity.getFileExtension() + entry = categories[note.category] + "/sketch/" + name + "_" + note._id + "_" + SketchActivity.getFileExtension() inputStream = FileInputStream(File(filesDir.path + "/sketches" + note.content)) } } From f778a049d688d5779a1c56e6eee6f6b97084a3df Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 11:56:09 +0100 Subject: [PATCH 03/26] [fix] Pressing enter did not add item anymore. --- .../privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt index 50a9b47..dad94e8 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt @@ -19,6 +19,7 @@ import android.os.Bundle import android.text.Html import android.text.SpannedString import android.view.ContextThemeWrapper +import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View @@ -72,7 +73,7 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI override fun onLoadActivity() { etNewItem.setOnEditorActionListener { _, _, event -> - if (event == null && etNewItem.text.isNotEmpty()) { + if ((event == null || event.keyCode == KeyEvent.KEYCODE_ENTER) && etNewItem.text.isNotEmpty()) { addItem() } return@setOnEditorActionListener true From 9655094448b5dff8ffc7b72f5c593ebbff2b6073 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 12:35:51 +0100 Subject: [PATCH 04/26] [fix] Allow ArrowKey movement as well as clickable links. --- .../helper/ArrowKeyLinkTouchMovementMethod.kt | 59 +++++++++++++++++++ .../ui/notes/TextNoteActivity.kt | 3 +- .../main/res/layout/activity_text_note.xml | 13 ++-- 3 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/ArrowKeyLinkTouchMovementMethod.kt diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/ArrowKeyLinkTouchMovementMethod.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/ArrowKeyLinkTouchMovementMethod.kt new file mode 100644 index 0000000..874b06b --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/ArrowKeyLinkTouchMovementMethod.kt @@ -0,0 +1,59 @@ +package org.secuso.privacyfriendlynotes.ui.helper + +import android.text.Selection +import android.text.Spannable +import android.text.method.ArrowKeyMovementMethod +import android.text.style.ClickableSpan +import android.view.MotionEvent +import android.widget.TextView + +class ArrowKeyLinkTouchMovementMethod : ArrowKeyMovementMethod() { + + override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean { + val action = event.action + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { + var x = event.x.toInt() + var y = event.y.toInt() + x -= widget.totalPaddingLeft + y -= widget.totalPaddingTop + + x += widget.scrollX + y += widget.scrollY + + val offset = widget.layout.let { + it.getOffsetForHorizontal( + it.getLineForVertical(y), + x.toFloat() + ) + } + + val link = buffer.getSpans(offset, offset, ClickableSpan::class.java) + + if (link.isNotEmpty()) { + if (action == MotionEvent.ACTION_UP) { + link[0].onClick(widget) + } else { + Selection.setSelection( + buffer, + buffer.getSpanStart(link[0]), + buffer.getSpanEnd(link[0]) + ) + } + return true + } + } + return super.onTouchEvent(widget, buffer, event) + } + + companion object { + private var instance: ArrowKeyLinkTouchMovementMethod? = null + + fun getInstance(): ArrowKeyLinkTouchMovementMethod { + if (instance == null) { + instance = ArrowKeyLinkTouchMovementMethod() + } + return instance!! + } + } +} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index 4875d00..9fd22d0 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -45,6 +45,7 @@ import kotlinx.coroutines.launch import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note +import org.secuso.privacyfriendlynotes.ui.helper.ArrowKeyLinkTouchMovementMethod import org.secuso.privacyfriendlynotes.ui.util.ChecklistUtil import java.io.File import java.io.InputStreamReader @@ -110,7 +111,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } } - etContent.movementMethod = LinkMovementMethod.getInstance() + etContent.movementMethod = ArrowKeyLinkTouchMovementMethod.getInstance() super.onCreate(savedInstanceState) } diff --git a/app/src/main/res/layout/activity_text_note.xml b/app/src/main/res/layout/activity_text_note.xml index b4442af..5c8fc5d 100644 --- a/app/src/main/res/layout/activity_text_note.xml +++ b/app/src/main/res/layout/activity_text_note.xml @@ -18,23 +18,18 @@ - - + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp"/> From 0e694af097eb734d4ee2d0379576849351957eab Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 12:57:26 +0100 Subject: [PATCH 05/26] [fix] remember cursor location. --- .../privacyfriendlynotes/ui/notes/TextNoteActivity.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index 9fd22d0..ea7711e 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -61,6 +61,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val boldBtn: FloatingActionButton by lazy { findViewById(R.id.btn_bold) } private val italicsBtn: FloatingActionButton by lazy { findViewById(R.id.btn_italics) } private val underlineBtn: FloatingActionButton by lazy { findViewById(R.id.btn_underline) } + private var lastCursorPosition = 0 private val isBold = MutableLiveData(false) private val isItalic = MutableLiveData(false) @@ -117,6 +118,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { override fun onNoteLoadedFromDB(note: Note) { etContent.setText(Html.fromHtml(note.content)) + etContent.setSelection(lastCursorPosition.coerceIn(0, etContent.text.length)) oldText = etContent.text.toString() } @@ -226,6 +228,11 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } } + override fun onPause() { + lastCursorPosition = etContent.selectionStart + super.onPause() + } + override fun onClick(v: View) { val startSelection: Int val endSelection: Int From 2afd887c6600e9ead5524cf9c419d2c6f51cf62f Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 13:21:02 +0100 Subject: [PATCH 06/26] [feat] makes text note FAB draggable --- .../ui/helper/DraggableFAB.kt | 59 +++++++++++++++++++ .../ui/notes/TextNoteActivity.kt | 2 + app/src/main/res/layout/text_note_fab.xml | 17 ++---- 3 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/DraggableFAB.kt diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/DraggableFAB.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/DraggableFAB.kt new file mode 100644 index 0000000..bc524b8 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/DraggableFAB.kt @@ -0,0 +1,59 @@ +package org.secuso.privacyfriendlynotes.ui.helper + +import android.view.MotionEvent +import android.view.View +import com.google.android.material.floatingactionbutton.FloatingActionButton +import kotlin.math.abs + +fun FloatingActionButton.makeDraggable(target: View = this) { + var downX = 0f + var downY = 0f + var dX = 0f + var dY = 0f + + val CLICK_DRAG_TOLERANCE = 10f + + this.setOnTouchListener { _, event -> + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + downX = event.rawX + downY = event.rawY + dX = target.x - downX + dY = target.y - downY + true + } + + MotionEvent.ACTION_MOVE -> { + val viewWidth = target.width + val viewHeight = target.height + + val viewParent = target.parent as View + val parentWidth = viewParent.width + val parentHeight = viewParent.height + + target.animate() + .x((parentWidth - viewWidth).toFloat().coerceAtMost(event.rawX + dX)) + .y((parentHeight - viewHeight).toFloat().coerceAtMost(event.rawY + dY)) + .setDuration(0) + .start() + true + } + + MotionEvent.ACTION_UP -> { + val upRawX = event.rawX + val upRawY = event.rawY + + val distanceX = upRawX - downX + val distanceY = upRawY - downY + + // If the finger didn't move much, trigger a click + if (abs(distanceX) < CLICK_DRAG_TOLERANCE && abs(distanceY) < CLICK_DRAG_TOLERANCE) { + performClick() + } + true + } + + else -> false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index ea7711e..8d0ea45 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -46,6 +46,7 @@ import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note import org.secuso.privacyfriendlynotes.ui.helper.ArrowKeyLinkTouchMovementMethod +import org.secuso.privacyfriendlynotes.ui.helper.makeDraggable import org.secuso.privacyfriendlynotes.ui.util.ChecklistUtil import java.io.File import java.io.InputStreamReader @@ -78,6 +79,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { val fabMenuBtn = findViewById(R.id.fab_menu) val fabMenu = findViewById(R.id.fab_menu_wrapper) + fabMenuBtn.makeDraggable(fabMenuBtn.parent as View) var expanded = false fabMenuBtn.setOnClickListener { if (expanded) { diff --git a/app/src/main/res/layout/text_note_fab.xml b/app/src/main/res/layout/text_note_fab.xml index c8d7631..24b6885 100644 --- a/app/src/main/res/layout/text_note_fab.xml +++ b/app/src/main/res/layout/text_note_fab.xml @@ -1,5 +1,5 @@ - - - - - + From 67938b4ff9a2033b2218eb6ef9ca9fc1bea4d334 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 13:31:29 +0100 Subject: [PATCH 07/26] [feat] allows to copy text from locked text note. --- .../ui/notes/TextNoteActivity.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index 8d0ea45..ccb6d44 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -21,10 +21,12 @@ import android.graphics.Typeface import android.net.Uri import android.os.Bundle import android.text.Html +import android.text.InputType import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import android.text.method.LinkMovementMethod +import android.text.method.TextKeyListener import android.text.style.StyleSpan import android.text.style.UnderlineSpan import android.view.ContextThemeWrapper @@ -108,8 +110,15 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - isLocked.collect { - etContent.isEnabled = !it + isLocked.collect { readonly -> + if (readonly) { + etContent.keyListener = null + etContent.showSoftInputOnFocus = false + } else { + etContent.keyListener = TextKeyListener.getInstance() + etContent.showSoftInputOnFocus = true + etContent.movementMethod = ArrowKeyLinkTouchMovementMethod.getInstance() + } } } } From f43339b14d498a1b4bf63ee6c01f65b81ab74710 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Fri, 6 Mar 2026 13:53:50 +0100 Subject: [PATCH 08/26] [feat] allows creation of new checklist with all checked/unchecked items. --- .../ui/notes/BaseNoteActivity.kt | 14 ++++++++++++++ .../ui/notes/ChecklistNoteActivity.kt | 18 ++++++++++++++++++ app/src/main/res/menu/activity_checklist.xml | 10 ++++++++++ app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 46 insertions(+) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt index 78bf968..cc207c8 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt @@ -683,6 +683,20 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } } + fun newNote(content: String, type: Int, afterUpdate: (Int) -> Unit) { + saveNote(force = true) + shouldSaveOnPause = false + createEditNoteViewModel.getNoteByID(id.toLong()).observe(this) { + if (it != null) { + it.content = content + it.type = type + it._id = 0 + val id = createEditNoteViewModel.insert(it) + afterUpdate(id) + } + } + } + class ActionResult(private val status: Boolean, val ok: O?, val err: E? = null) { fun isOk(): Boolean { return this.status diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt index dad94e8..04ed274 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt @@ -141,6 +141,24 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI } R.id.action_select_all -> adapter.selectAll() R.id.action_deselect_all -> adapter.deselectAll() + R.id.action_new_checked -> { + val items = adapter.getItems().filter { it.state }.map { ChecklistItem(false, it.name) } + super.newNote(ChecklistUtil.json(items).toString(), DbContract.NoteEntry.TYPE_CHECKLIST) { + val i = Intent(application, ChecklistNoteActivity::class.java) + i.putExtra(EXTRA_ID, it) + startActivity(i) + finish() + } + } + R.id.action_new_unchecked -> { + val items = adapter.getItems().filter { !it.state } + super.newNote(ChecklistUtil.json(items).toString(), DbContract.NoteEntry.TYPE_CHECKLIST) { + val i = Intent(application, ChecklistNoteActivity::class.java) + i.putExtra(EXTRA_ID, it) + startActivity(i) + finish() + } + } else -> {} } diff --git a/app/src/main/res/menu/activity_checklist.xml b/app/src/main/res/menu/activity_checklist.xml index 930e8b7..67448b7 100644 --- a/app/src/main/res/menu/activity_checklist.xml +++ b/app/src/main/res/menu/activity_checklist.xml @@ -7,6 +7,16 @@ android:title="@string/action_convert_to_text" android:icon="@drawable/ic_short_text_icon_24dp" app:showAsAction="ifRoom"/> + + Alphabetisch sortieren Erinnerung setzen Alle auswählen + Neue Checkliste mit Offenen Speichern Privacy Friendly Notizen Entsperren @@ -173,4 +174,5 @@ Alle abwählen Sperren Diese Notiz konnte nicht geladen werden. Wenn Sie versuchen diese Notiz zu öffnen, wird stattdessen versucht den Inhalt der Notiz unformattiert zu speichern. + Neue Checkliste mit Fertigen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0c0f90..b195e49 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,6 +35,8 @@ Delete all Deselect all Select all + New checklist from checked + New checklist from unchecked Save Lock Unlock From bad1b62003fb03d4d0a3c55058a56cac6375b522 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Mon, 30 Mar 2026 20:25:08 +0200 Subject: [PATCH 09/26] [feat] allows to export sketch as png. --- .../ui/notes/BaseNoteActivity.kt | 29 +++++++++++++++---- .../ui/notes/SketchActivity.kt | 18 ++++++++++++ app/src/main/res/menu/activity_sketch.xml | 5 ++++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt index cc207c8..188a476 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt @@ -117,6 +117,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli private var id = -1 private val isLockedState: MutableStateFlow = MutableStateFlow(false) protected val isLocked: StateFlow = isLockedState + private var initialLockState: Boolean? = null private var lockedItem: MenuItem? = null @@ -188,7 +189,9 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli etName.isEnabled = !isLocked.value catSelection.isEnabled = !isLocked.value - hasChanged = true + if (initialLockState == null) { + initialLockState = isLocked.value + } } } } @@ -385,6 +388,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli R.id.action_lock -> { isLockedState.value = !isLockedState.value + saveNote(force = true) } R.id.action_share -> { @@ -453,7 +457,9 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli @Deprecated("Deprecated in Java") override fun onBackPressed() { - shouldSaveOnPause = true + if (hasChanged || initialLockState != isLocked.value) { + shouldSaveOnPause = true + } super.onBackPressed() } @@ -522,6 +528,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli id = createEditNoteViewModel.insert(note) Toast.makeText(applicationContext, R.string.toast_saved, Toast.LENGTH_SHORT).show() } + initialLockState = isLocked.value return true } @@ -592,12 +599,17 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } } + private var saveToExternalStorageCustomAction: ((OutputStream) -> Unit)? = null private val saveToExternalStorageResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> val fileOutputStream: OutputStream? = contentResolver.openOutputStream(uri) fileOutputStream?.let { - onSaveExternalStorage(it) + if (saveToExternalStorageCustomAction != null) { + saveToExternalStorageCustomAction?.invoke(it) + } else { + onSaveExternalStorage(it) + } Toast.makeText( applicationContext, String.format(getString(R.string.toast_file_exported_to), uri.toString()), @@ -609,11 +621,16 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } } - private fun saveToExternalStorage() { + protected fun saveToExternalStorage( + extension: String? = null, + mimeType: String? = null, + saveToExternalStorageAction: ((OutputStream) -> Unit)? = null + ) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.putExtra(Intent.EXTRA_TITLE, etName.text.toString() + getFileExtension()) - intent.type = getMimeType() + intent.putExtra(Intent.EXTRA_TITLE, etName.text.toString() + (extension ?: getFileExtension())) + intent.type = mimeType ?: getMimeType() + saveToExternalStorageCustomAction = saveToExternalStorageAction saveToExternalStorageResultLauncher.launch(intent) } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt index de20684..74a8c38 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt @@ -222,6 +222,24 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH), OnDia } } + R.id.action_export_png -> { + super.saveToExternalStorage( + extension = ".png", + mimeType = "image/png" + ) { + val bm = BitmapDrawable(resources, mFilePath).bitmap.overlay(drawView.bitmap) + val canvas = Canvas(bm) + canvas.drawColor(Color.WHITE) + canvas.drawBitmap( + BitmapDrawable(resources, mFilePath).bitmap.overlay(drawView.bitmap), + 0f, + 0f, + null + ) + bm.compress(Bitmap.CompressFormat.PNG, 100, it) + } + } + else -> {} } return super.onOptionsItemSelected(item) diff --git a/app/src/main/res/menu/activity_sketch.xml b/app/src/main/res/menu/activity_sketch.xml index c41bb53..ef774e8 100644 --- a/app/src/main/res/menu/activity_sketch.xml +++ b/app/src/main/res/menu/activity_sketch.xml @@ -15,4 +15,9 @@ android:icon="@drawable/ic_baseline_redo_icon_24" app:iconTint="@color/menu_state_list" app:showAsAction="always"/> + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1c7d194..28968b6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -79,6 +79,7 @@ Exportieren Alle Notizen exportieren Als HTML exportieren + Als PNG exportieren Kategorie In Checkliste umwandeln In Textnotiz umwandeln diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b195e49..cb13aa6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Export Export all notes Export as HTML + Export as PNG Category Undo Redo From b636a15394ec0c6d553d6e82044fbdd165817f75 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Mon, 30 Mar 2026 20:39:13 +0200 Subject: [PATCH 10/26] [feat] allows toggling of checklist item strike-through. --- .../privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt | 5 +++-- app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/pref_settings.xml | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt index fea1486..6917486 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt @@ -77,6 +77,7 @@ class ChecklistAdapter( override fun onBindViewHolder(holder: ItemHolder, position: Int) { val (checked, item) = items[position] + val strikeThrough = PreferenceManager.getDefaultSharedPreferences(holder.itemView.context).getBoolean("settings_checklist_strike_items", true) holder.textView.text = item holder.checkbox.isChecked = checked holder.dragHandle.setOnTouchListener { v, _ -> @@ -86,7 +87,7 @@ class ChecklistAdapter( holder.checkbox.setOnClickListener { _ -> items[holder.bindingAdapterPosition].state = holder.checkbox.isChecked holder.textView.apply { - paintFlags = if (holder.checkbox.isChecked) { + paintFlags = if (holder.checkbox.isChecked && strikeThrough) { paintFlags or Paint.STRIKE_THRU_TEXT_FLAG } else { paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() @@ -111,7 +112,7 @@ class ChecklistAdapter( }) holder.textView.apply { - paintFlags = if (checked) { + paintFlags = if (checked && strikeThrough) { paintFlags or Paint.STRIKE_THRU_TEXT_FLAG } else { paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 28968b6..206cdbb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -176,4 +176,6 @@ Sperren Diese Notiz konnte nicht geladen werden. Wenn Sie versuchen diese Notiz zu öffnen, wird stattdessen versucht den Inhalt der Notiz unformattiert zu speichern. Neue Checkliste mit Fertigen + Einträge Checkliste durchstreichen + Streiche den Beschreibungstext eines Checklisteneintrages durch, sofern dieser abgehakt wurde. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb13aa6..8e103d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -200,4 +200,6 @@ Character limit for imported text notes File Size limit for imported text notes This note could not be loaded. Click to save the stored content. + Strike checked items in checklist + If enabled, checked checklist items will be rendered with a strike-through line. diff --git a/app/src/main/res/xml/pref_settings.xml b/app/src/main/res/xml/pref_settings.xml index e74aad7..9d869a4 100644 --- a/app/src/main/res/xml/pref_settings.xml +++ b/app/src/main/res/xml/pref_settings.xml @@ -94,5 +94,11 @@ android:title="@string/settings_settings_show_preview_title" android:summary="@string/settings_settings_settings_show_preview_sum" app:iconSpaceReserved="false" /> + \ No newline at end of file From 5ae0c65c85d2e9b2db0df32e6c4142c8367c8aa9 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 31 Mar 2026 11:29:00 +0200 Subject: [PATCH 11/26] [feat] Adds option to insert picture or take photo into text note. --- app/src/main/AndroidManifest.xml | 1 + .../ui/notes/BaseNoteActivity.kt | 8 +- .../ui/notes/TextNoteActivity.kt | 95 ++++++++++++++++++- .../res/drawable/ic_photo_camera_24px.xml | 5 + app/src/main/res/layout/text_note_fab.xml | 24 +++++ app/src/main/res/xml/fileprovider_paths.xml | 2 + 6 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/drawable/ic_photo_camera_24px.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 06f5205..87f3924 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + = MutableStateFlow(false) protected val isLocked: StateFlow = isLockedState private var initialLockState: Boolean? = null @@ -463,11 +464,6 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli super.onBackPressed() } - override fun onResume() { - super.onResume() - loadActivity(false) - } - override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index ccb6d44..38db69e 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -16,8 +16,10 @@ package org.secuso.privacyfriendlynotes.ui.notes import android.app.Activity import android.content.Intent import android.content.res.ColorStateList +import android.graphics.Bitmap import android.graphics.Color import android.graphics.Typeface +import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import android.text.Html @@ -29,13 +31,17 @@ import android.text.method.LinkMovementMethod import android.text.method.TextKeyListener import android.text.style.StyleSpan import android.text.style.UnderlineSpan +import android.util.Log import android.view.ContextThemeWrapper import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.EditText import android.widget.Toast +import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.FileProvider +import androidx.core.text.HtmlCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope @@ -51,9 +57,16 @@ import org.secuso.privacyfriendlynotes.ui.helper.ArrowKeyLinkTouchMovementMethod import org.secuso.privacyfriendlynotes.ui.helper.makeDraggable import org.secuso.privacyfriendlynotes.ui.util.ChecklistUtil import java.io.File +import java.io.FileOutputStream import java.io.InputStreamReader import java.io.OutputStream import java.io.PrintWriter +import java.util.jar.Manifest +import kotlin.io.path.exists +import androidx.core.text.toHtml +import androidx.core.text.parseAsHtml +import java.util.UUID +import kotlin.random.Random /** * Activity that allows to add, edit and delete text notes. @@ -64,6 +77,8 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val boldBtn: FloatingActionButton by lazy { findViewById(R.id.btn_bold) } private val italicsBtn: FloatingActionButton by lazy { findViewById(R.id.btn_italics) } private val underlineBtn: FloatingActionButton by lazy { findViewById(R.id.btn_underline) } + private val galleryBtn: FloatingActionButton by lazy { findViewById(R.id.btn_gallery) } + private val cameraBtn: FloatingActionButton by lazy { findViewById(R.id.btn_camera) } private var lastCursorPosition = 0 private val isBold = MutableLiveData(false) @@ -76,6 +91,70 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val fileSizeLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_size_limit", "10000")?.toInt() ?: 10000 } private val fileCharLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_char_limit", "1000")?.toInt() ?: 1000 } + val htmlImageGetter = Html.ImageGetter { source -> + try { + val file = File("${filesDir.path}/text_notes/${id}", source) + + if (file.exists()) { + val drawable = Drawable.createFromPath(file.absolutePath) + drawable?.let { + it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight) + } + return@ImageGetter drawable + } + } catch (e: Exception) { + e.printStackTrace() + } + null + } + + private var imageFile: String? = null + + val takePictureLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { + Log.d("TextNoteActivity", "Received picture callback with result $it") + if (!it || imageFile == null) { + return@registerForActivityResult + } + // image was successfully stored in our requested location + // so add the image to the text field + insertImageToText(imageFile!!) + } + val requestCameraPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) { + imageFile = System.currentTimeMillis().toString() + ".png" + File("${filesDir.path}/text_notes/${id}").mkdirs() + val file = File("${filesDir.path}/text_notes/${id}", imageFile!!) + val uri = FileProvider.getUriForFile(this, "org.secuso.privacyfriendlynotes", file) + Log.d("TextNoteActivity", "Now attempting to take picture for ${imageFile} and uri ${uri}") + takePictureLauncher.launch(uri) + } + } + + val pickImageLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + if (uri == null) { + return@registerForActivityResult + } + val file = "${System.currentTimeMillis()}.png" + val path = "${filesDir.path}/text_notes/${id}" + val inputStream = contentResolver.openInputStream(uri) + val outputStream = FileOutputStream(File(path, file)) + File("${filesDir.path}/text_notes/${id}").mkdirs() + inputStream.use { input -> outputStream.use { input?.copyTo(it) } } + insertImageToText(file) + } + + private fun insertImageToText(file: String) { + // Use a random anchor to obtain the current cursor location and insert the image there. + val anchor = UUID.randomUUID().toString() + var html = SpannableStringBuilder(etContent.text).apply { + insert(etContent.selectionStart, anchor) + }.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL) + html = html.replace(anchor, "
\"${file}\"") + etContent.setText(html.parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter)) + etContent.setSelection(lastCursorPosition.coerceIn(0, etContent.text.length)) + Log.d("TextNoteActivity", "Successfully taken picture and inserted into text note") + } + override fun onCreate(savedInstanceState: Bundle?) { setContentView(R.layout.activity_text_note) @@ -97,6 +176,13 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { boldBtn.setOnClickListener(this) italicsBtn.setOnClickListener(this) underlineBtn.setOnClickListener(this) + galleryBtn.setOnClickListener { + pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } + cameraBtn.setOnClickListener { + requestCameraPermission.launch(android.Manifest.permission.CAMERA) + } + isBold.observe(this) { b: Boolean -> boldBtn.backgroundTintList = ColorStateList.valueOf(if (b) Color.parseColor("#000000") else resources.getColor(R.color.colorSecuso)) @@ -128,7 +214,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } override fun onNoteLoadedFromDB(note: Note) { - etContent.setText(Html.fromHtml(note.content)) + etContent.setText(HtmlCompat.fromHtml(note.content, HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) etContent.setSelection(lastCursorPosition.coerceIn(0, etContent.text.length)) oldText = etContent.text.toString() } @@ -210,10 +296,10 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } } super.setTitle(Html.fromHtml(title).toString()) - etContent.setText(Html.fromHtml(text.joinToString("
"))) + etContent.setText(HtmlCompat.fromHtml(text.joinToString("
"), HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) } intent.getStringExtra(Intent.EXTRA_TEXT)?.let { - etContent.setText(Html.fromHtml(it)) + etContent.setText(HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) } } } @@ -379,6 +465,9 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { etContent.text = totalText etContent.setSelection(startSelection) } + R.id.btn_gallery -> { + + } else -> {} } diff --git a/app/src/main/res/drawable/ic_photo_camera_24px.xml b/app/src/main/res/drawable/ic_photo_camera_24px.xml new file mode 100644 index 0000000..e71e765 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_camera_24px.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/text_note_fab.xml b/app/src/main/res/layout/text_note_fab.xml index 24b6885..91b87e8 100644 --- a/app/src/main/res/layout/text_note_fab.xml +++ b/app/src/main/res/layout/text_note_fab.xml @@ -61,6 +61,30 @@ app:tint="@color/white" app:backgroundTint="@color/colorSecuso" app:fabSize="mini" /> + + + + \ No newline at end of file From e92b9321980914f04c4bf6fb2582b234239ec6ac Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 31 Mar 2026 11:37:02 +0200 Subject: [PATCH 12/26] [feat] Automatically delete unused files after closing text note. --- .../ui/notes/TextNoteActivity.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index 38db69e..5ed8358 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -91,6 +91,8 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val fileSizeLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_size_limit", "10000")?.toInt() ?: 10000 } private val fileCharLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_char_limit", "1000")?.toInt() ?: 1000 } + // Remember all loaded images to delete all not used images at activity end + private val loadedImages = mutableListOf() val htmlImageGetter = Html.ImageGetter { source -> try { val file = File("${filesDir.path}/text_notes/${id}", source) @@ -100,6 +102,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { drawable?.let { it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight) } + loadedImages.add(source) return@ImageGetter drawable } } catch (e: Exception) { @@ -330,6 +333,20 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { super.onPause() } + override fun onDestroy() { + File("${filesDir.path}/text_notes/${id}").apply { + if (exists() && isDirectory) { + listFiles()?.forEach { + if (!loadedImages.contains(it.name)) { + Log.d("TextNote", "Deleting file ${it.name}") + it.delete() + } + } + } + } + super.onDestroy() + } + override fun onClick(v: View) { val startSelection: Int val endSelection: Int From e61a670e871f4caffb7ba280b56b29bb1f9428c1 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 31 Mar 2026 11:55:03 +0200 Subject: [PATCH 13/26] [feat] Allows images on new notes. --- .../ui/notes/TextNoteActivity.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index 5ed8358..4270077 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -65,7 +65,11 @@ import java.util.jar.Manifest import kotlin.io.path.exists import androidx.core.text.toHtml import androidx.core.text.parseAsHtml +import java.nio.file.StandardCopyOption import java.util.UUID +import kotlin.io.path.Path +import kotlin.io.path.moveTo +import kotlin.properties.Delegates import kotlin.random.Random /** @@ -91,6 +95,9 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val fileSizeLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_size_limit", "10000")?.toInt() ?: 10000 } private val fileCharLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_char_limit", "1000")?.toInt() ?: 1000 } + // Remember initialId to move images to correct id if this was a new note. + private var initialId by Delegates.notNull() + // Remember all loaded images to delete all not used images at activity end private val loadedImages = mutableListOf() val htmlImageGetter = Html.ImageGetter { source -> @@ -220,6 +227,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { etContent.setText(HtmlCompat.fromHtml(note.content, HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) etContent.setSelection(lastCursorPosition.coerceIn(0, etContent.text.length)) oldText = etContent.text.toString() + initialId = super.id } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -305,6 +313,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { etContent.setText(HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) } } + initialId = super.id } override fun onLoadActivity() { @@ -334,6 +343,21 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } override fun onDestroy() { + if (initialId != id) { + val source = File("${filesDir.path}/text_notes/${initialId}") + val target = File("${filesDir.path}/text_notes/${id}") + + if (source.isDirectory) { + if (!target.exists()) { + target.mkdirs() + } + + source.listFiles()?.forEach { + it.copyTo(File(target, it.name)) + } + } + source.delete() + } File("${filesDir.path}/text_notes/${id}").apply { if (exists() && isDirectory) { listFiles()?.forEach { From 0851769598a0e09ea3cb773bd14d9e38bef48f32 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 31 Mar 2026 13:40:58 +0200 Subject: [PATCH 14/26] [feat] Correctly exports text notes with images. --- .../ui/main/MainActivityViewModel.kt | 8 +- .../ui/notes/TextNoteActivity.kt | 99 ++++++++++++++----- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt index 3f8d115..21b4956 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt @@ -42,6 +42,7 @@ import org.secuso.privacyfriendlynotes.ui.notes.SketchActivity import org.secuso.privacyfriendlynotes.ui.notes.TextNoteActivity import org.secuso.privacyfriendlynotes.ui.util.ChecklistUtil import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException @@ -271,8 +272,11 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica lateinit var inputStream: InputStream when(note.type) { DbContract.NoteEntry.TYPE_TEXT -> { - entry = categories[note.category] + "/text/" + name + "_" + note._id + "_" + TextNoteActivity.getFileExtension() - inputStream = ByteArrayInputStream(note.content.toByteArray()) + entry = categories[note.category] + "/text/" + name + "_" + note._id + "_" + TextNoteActivity.getFileExtension(filesDir, note._id) + // This could be a problem for large Notes, so if the app crashes due to OOM + val out = ByteArrayOutputStream() + TextNoteActivity.exportWithImages(filesDir, note.content, note._id, out) + inputStream = ByteArrayInputStream(out.toByteArray()) } DbContract.NoteEntry.TYPE_CHECKLIST -> { entry = categories[note.category] + "/checklist/" + name + "_" + note._id + "_" + ChecklistNoteActivity.getFileExtension() diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index 4270077..ae7575c 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -14,6 +14,7 @@ package org.secuso.privacyfriendlynotes.ui.notes import android.app.Activity +import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap @@ -65,8 +66,13 @@ import java.util.jar.Manifest import kotlin.io.path.exists import androidx.core.text.toHtml import androidx.core.text.parseAsHtml +import java.io.ByteArrayInputStream +import java.io.DataInputStream +import java.io.FileInputStream import java.nio.file.StandardCopyOption import java.util.UUID +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import kotlin.io.path.Path import kotlin.io.path.moveTo import kotlin.properties.Delegates @@ -102,7 +108,9 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val loadedImages = mutableListOf() val htmlImageGetter = Html.ImageGetter { source -> try { - val file = File("${filesDir.path}/text_notes/${id}", source) + // This is intentionally not getImageFilePathForId as we want to produce a correct html file on export, + // Which points correctly to the images directory next to the exported source file. + val file = File("${filesDir.path}/text_notes/${id}/images", source) if (file.exists()) { val drawable = Drawable.createFromPath(file.absolutePath) @@ -132,11 +140,13 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { val requestCameraPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { if (it) { imageFile = System.currentTimeMillis().toString() + ".png" - File("${filesDir.path}/text_notes/${id}").mkdirs() - val file = File("${filesDir.path}/text_notes/${id}", imageFile!!) - val uri = FileProvider.getUriForFile(this, "org.secuso.privacyfriendlynotes", file) - Log.d("TextNoteActivity", "Now attempting to take picture for ${imageFile} and uri ${uri}") - takePictureLauncher.launch(uri) + getImageFilePathForId(id).apply { + mkdirs() + val file = File(this, imageFile!!) + val uri = FileProvider.getUriForFile(this@TextNoteActivity, "org.secuso.privacyfriendlynotes", file) + Log.d("TextNoteActivity", "Now attempting to take picture for ${imageFile} and uri ${uri}") + takePictureLauncher.launch(uri) + } } } @@ -145,11 +155,12 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { return@registerForActivityResult } val file = "${System.currentTimeMillis()}.png" - val path = "${filesDir.path}/text_notes/${id}" val inputStream = contentResolver.openInputStream(uri) - val outputStream = FileOutputStream(File(path, file)) - File("${filesDir.path}/text_notes/${id}").mkdirs() - inputStream.use { input -> outputStream.use { input?.copyTo(it) } } + getImageFilePathForId(id).apply { + mkdirs() + val outputStream = FileOutputStream(File(this, file)) + inputStream.use { input -> outputStream.use { input?.copyTo(it) } } + } insertImageToText(file) } @@ -343,9 +354,9 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } override fun onDestroy() { + val target = getImageFilePathForId(id) if (initialId != id) { - val source = File("${filesDir.path}/text_notes/${initialId}") - val target = File("${filesDir.path}/text_notes/${id}") + val source = getImageFilePathForId(initialId) if (source.isDirectory) { if (!target.exists()) { @@ -358,7 +369,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } source.delete() } - File("${filesDir.path}/text_notes/${id}").apply { + target.apply { if (exists() && isDirectory) { listFiles()?.forEach { if (!loadedImages.contains(it.name)) { @@ -631,18 +642,24 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } } - override fun getMimeType() = "text/plain" + override fun getMimeType() = if (loadedImages.isEmpty()) { + "text/plain" + } else { + "application/zip" + } - override fun getFileExtension() = TextNoteActivity.getFileExtension() + override fun getFileExtension() = if (loadedImages.isEmpty()) { + TextNoteActivity.getFileExtension() + } else { + ".zip" + } private val saveToExternalStorageResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> val fileOutputStream: OutputStream? = contentResolver.openOutputStream(uri) - fileOutputStream?.let { - val out = PrintWriter(it) - out.println(Html.toHtml(etContent.text)) - out.close() + fileOutputStream?.let { outputStream -> + exportWithImages(filesDir, etContent.text.toHtml(), id, outputStream) Toast.makeText( applicationContext, String.format(getString(R.string.toast_file_exported_to), uri.toString()), @@ -655,12 +672,48 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } override fun onSaveExternalStorage(outputStream: OutputStream) { - val out = PrintWriter(outputStream) - out.println(Html.fromHtml(Html.toHtml(etContent.text)).toString()) - out.close() + exportWithImages(filesDir, etContent.text.toHtml(), id, outputStream) } + private fun getImageFilePathForId(id: Int) = getImageFilePathForId(filesDir, id) + companion object { - fun getFileExtension() = ".txt" + + private fun getImageFilePathForId(filesDir: File, id: Int): File { + val path = File("${filesDir}/text_notes/$id/images") + return path + } + fun getFileExtension(filesDir: File? = null, id: Int? = null): String { + if (id != null && filesDir != null) { + val dir = getImageFilePathForId(filesDir, id) + return if (dir.exists() && dir.isDirectory && dir.listFiles()?.isNotEmpty() == true) { + ".zip" + } else { + ".txt" + } + } else { + return ".txt" + } + } + + fun exportWithImages(filesDir: File, content: String, id: Int, outputStream: OutputStream, zipped: Boolean = true) { + val dir = getImageFilePathForId(filesDir, id) + if (dir.exists() && dir.isDirectory) { + ZipOutputStream(outputStream).use { + it.putNextEntry(ZipEntry("text.txt")) + ByteArrayInputStream(content.toByteArray()).copyTo(it) + it.closeEntry() + dir.listFiles()?.forEach { file -> + it.putNextEntry(ZipEntry("images/${file}")) + FileInputStream(file).copyTo(it) + it.closeEntry() + } + } + } else { + PrintWriter(outputStream).use { + println(content) + } + } + } } } \ No newline at end of file From 3f2fad4329f3b349d59c7d16d0b1e6575314a1f2 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 31 Mar 2026 21:39:15 +0200 Subject: [PATCH 15/26] [feat] Correctly save & restore text notes with images. [fix] Creating a new note with text and then adding an image caused to problems due to temporary default ids. --- .../backup/BackupCreator.java | 15 +++++++++ .../backup/BackupRestorer.java | 1 + .../privacyfriendlynotes/room/dao/NoteDao.kt | 3 ++ .../ui/notes/BaseNoteActivity.kt | 13 +++++++- .../ui/notes/CreateEditNoteViewModel.kt | 4 +++ .../ui/notes/TextNoteActivity.kt | 33 ++++--------------- 6 files changed, 42 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java index 60166c6..23a5d8c 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java @@ -35,6 +35,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.Arrays; +import java.util.List; public class BackupCreator implements IBackupCreator { @@ -68,6 +69,20 @@ public boolean writeBackup(@NonNull Context context, @NonNull OutputStream outpu writer.name(path); FileUtil.writePath(writer, new File(context.getFilesDir().getPath(), path), false); } + File text_notes = new File(context.getFilesDir().getPath(), "text_notes"); + if (text_notes.exists() && text_notes.isDirectory()) { + File[] files = text_notes.listFiles(); + if (files != null) { + writer.name("text_notes"); + writer.beginObject(); + for (File path : files) { + Log.d("PFA BackupCreator", "Writing images of text note " + path); + writer.name(path.getName()); + FileUtil.writePath(writer, path, true); + } + writer.endObject(); + } + } Log.d("PFA BackupCreator", "finished writing files"); writer.endObject(); diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java index 569687c..cb305f4 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java @@ -46,6 +46,7 @@ private void readFiles(@NonNull JsonReader reader, @NonNull Context context) thr switch (name) { case "sketches": + case "text_notes": case "audio_notes": File f = new File(context.getFilesDir(), name); FileUtil.readPath(reader, f); diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt index da0921f..9aeddd4 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt @@ -53,4 +53,7 @@ interface NoteDao { @Query("SELECT * FROM notes WHERE _id = :id") fun getNoteByID(id: Long): Note? + + @Query("SELECT seq + 1 FROM sqlite_sequence WHERE name = :tableName") + fun getNextId(tableName: String = "notes"): Int } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt index a9d3a11..b5c4739 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt @@ -130,6 +130,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli private val noteType by lazy { noteType } protected abstract fun onNoteSave(name: String, category: Int): ActionResult + protected open fun onNoteWasSaved() {} protected abstract fun onLoadActivity() protected abstract fun onSaveExternalStorage(outputStream: OutputStream) @@ -252,6 +253,12 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } isLoadedNote = id != -1 + if (id == -1) { + // We do not want to keep this default value as it does not behave nicely with the activity lifecycle if the activity depends on the note id + // e.g. storing files associated to the id like images for text notes. + id = createEditNoteViewModel.nextNoteId() + } + // Should we set a custom font size? val sp = PreferenceManager.getDefaultSharedPreferences(this) if (sp.getBoolean(SettingsActivity.PREF_CUSTOM_FONT, false)) { @@ -265,6 +272,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli displayCategoryDialog() } + //fill in values if update if (isLoadedNote) { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) @@ -509,8 +517,8 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli etName.setText(note.name) } note.readonly = if (isLocked.value) 1 else 0 + note._id = id if (isLoadedNote) { - note._id = id if (showNotSaved) { //Wait for job to complete runBlocking { @@ -523,8 +531,11 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } else { id = createEditNoteViewModel.insert(note) Toast.makeText(applicationContext, R.string.toast_saved, Toast.LENGTH_SHORT).show() + // ensure that the note is only inserted once, even if the activity was paused + isLoadedNote = true } initialLockState = isLocked.value + onNoteWasSaved() return true } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt index 115953e..9eef6fb 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt @@ -102,6 +102,10 @@ class CreateEditNoteViewModel(application: Application) : AndroidViewModel(appli return _categoryName } + fun nextNoteId(): Int { + return database.noteDao().getNextId() + } + /** * Returns id */ diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index ae7575c..c39c188 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -101,9 +101,6 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val fileSizeLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_size_limit", "10000")?.toInt() ?: 10000 } private val fileCharLimit by lazy { PreferenceManager.getDefaultSharedPreferences(this@TextNoteActivity).getString("settings_import_text_file_char_limit", "1000")?.toInt() ?: 1000 } - // Remember initialId to move images to correct id if this was a new note. - private var initialId by Delegates.notNull() - // Remember all loaded images to delete all not used images at activity end private val loadedImages = mutableListOf() val htmlImageGetter = Html.ImageGetter { source -> @@ -238,7 +235,6 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { etContent.setText(HtmlCompat.fromHtml(note.content, HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) etContent.setSelection(lastCursorPosition.coerceIn(0, etContent.text.length)) oldText = etContent.text.toString() - initialId = super.id } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -324,7 +320,6 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { etContent.setText(HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) } } - initialId = super.id } override fun onLoadActivity() { @@ -348,27 +343,9 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } } - override fun onPause() { - lastCursorPosition = etContent.selectionStart - super.onPause() - } - - override fun onDestroy() { + override fun onNoteWasSaved() { val target = getImageFilePathForId(id) - if (initialId != id) { - val source = getImageFilePathForId(initialId) - - if (source.isDirectory) { - if (!target.exists()) { - target.mkdirs() - } - - source.listFiles()?.forEach { - it.copyTo(File(target, it.name)) - } - } - source.delete() - } + // cleanup not used files target.apply { if (exists() && isDirectory) { listFiles()?.forEach { @@ -379,7 +356,11 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } } } - super.onDestroy() + } + + override fun onPause() { + lastCursorPosition = etContent.selectionStart + super.onPause() } override fun onClick(v: View) { From 0aaaeca02a033d1067f32a9006328a75638e1eff Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Wed, 1 Apr 2026 14:34:59 +0200 Subject: [PATCH 16/26] fixes lint. --- app/src/main/AndroidManifest.xml | 4 ++++ app/src/main/res/layout/text_note_fab.xml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87f3924..827f692 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + diff --git a/app/src/main/res/layout/text_note_fab.xml b/app/src/main/res/layout/text_note_fab.xml index 91b87e8..47bda39 100644 --- a/app/src/main/res/layout/text_note_fab.xml +++ b/app/src/main/res/layout/text_note_fab.xml @@ -18,6 +18,7 @@ android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/fab_menu" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" android:orientation="vertical"> Date: Mon, 13 Apr 2026 11:57:53 +0200 Subject: [PATCH 17/26] [feat] Automatically deletes trashed notes older than 7 days. [fix] Deleting text notes will now delete the stored images. TODO: Add preference to customize deletion time and disable feature. --- .../privacyfriendlynotes/room/NoteDatabase.java | 16 +++++++++++++++- .../privacyfriendlynotes/room/dao/NoteDao.kt | 3 +++ .../privacyfriendlynotes/room/model/Note.kt | 2 ++ .../privacyfriendlynotes/ui/main/MainActivity.kt | 6 ++++++ .../ui/main/MainActivityViewModel.kt | 11 +++++++++++ .../ui/notes/TextNoteActivity.kt | 2 +- 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java index 16504fb..aaeeca0 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java @@ -53,8 +53,22 @@ ) public abstract class NoteDatabase extends RoomDatabase { - public static final int VERSION = 7; + public static final int VERSION = 8; public static final String DATABASE_NAME = "allthenotes"; + static final Migration MIGRATION_7_8 = new Migration(7, 8) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE notes ADD COLUMN in_trash_time INTEGER NOT NULL DEFAULT 0;"); + + database.execSQL( + "CREATE TRIGGER [UpdateTrashTime] AFTER UPDATE ON notes FOR EACH ROW " + + "WHEN NEW.last_modified = OLD.last_modified AND NEW.custom_order = OLD.custom_order AND NEW.in_trash != OLD.in_trash " + + "BEGIN " + + "UPDATE notes SET in_trash_time = (CASE NEW.in_trash WHEN 0 THEN 0 ELSE DateTime('now') END) WHERE _id=NEW._id; " + + "END;" + ); + } + }; static final Migration MIGRATION_6_7 = new Migration(6, 7) { @Override diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt index 9aeddd4..9a1ef94 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt @@ -56,4 +56,7 @@ interface NoteDao { @Query("SELECT seq + 1 FROM sqlite_sequence WHERE name = :tableName") fun getNextId(tableName: String = "notes"): Int + + @Query("SELECT * FROM notes WHERE in_trash = 1 AND in_trash_time <= :timestamp") + fun getAllTrashedNotesOlderThan(timestamp: Long): List } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt index 3c1f63e..b1f195f 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt @@ -33,6 +33,8 @@ data class Note( var last_modified: Long, var custom_order: Int, var readonly: Int + var readonly: Int, + var in_trash_time: Long = 0, ) { constructor(name: String, content: String, type: Int, category: Int) : this( diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt index 49259c1..fb9d7d3 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -228,6 +228,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } } + // Delete all trashed notes which are old enough + lifecycleScope.launch { + val time = 7 * 24 * 60 * 60 * 1000L + mainActivityViewModel.deleteOldTrashedNotes(time) + } + val ith = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { val to = target.bindingAdapterPosition diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt index 21b4956..33942ed 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt @@ -151,6 +151,8 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica } else if (note.type == DbContract.NoteEntry.TYPE_SKETCH) { File(filesDir.path + "/sketches" + note.content).delete() File(filesDir.path + "/sketches" + note.content.substring(0, note.content.length - 3) + "jpg").delete() + } else if (note.type == DbContract.NoteEntry.TYPE_TEXT) { + TextNoteActivity.getImageFilePathForId(filesDir, note._id).delete() } } } @@ -228,6 +230,15 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica } } + fun deleteOldTrashedNotes(age: Long) { + viewModelScope.launch(Dispatchers.Default) { + repository.noteDao().getAllTrashedNotesOlderThan(System.currentTimeMillis() - age) + .forEach { + delete(it) + } + } + } + private fun loadSketchBitmap(file: String): BitmapDrawable? { File("${filesDir.path}/sketches${file}").apply { if (exists()) { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index c39c188..aa80948 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -660,7 +660,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { companion object { - private fun getImageFilePathForId(filesDir: File, id: Int): File { + fun getImageFilePathForId(filesDir: File, id: Int): File { val path = File("${filesDir}/text_notes/$id/images") return path } From f9566e574dc843a50dfff8d3f96ea6010617780e Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Mon, 13 Apr 2026 12:36:05 +0200 Subject: [PATCH 18/26] [feat] Adds settings to disable and adjust auto delete. [fix] Every note without timespan was deleted immediately. --- .../privacyfriendlynotes/room/NoteDatabase.java | 5 +++-- .../privacyfriendlynotes/room/dao/NoteDao.kt | 2 +- .../privacyfriendlynotes/room/model/Note.kt | 1 - .../privacyfriendlynotes/ui/main/MainActivity.kt | 8 ++++++-- app/src/main/res/values-de/strings.xml | 8 ++++++++ app/src/main/res/values/arrays.xml | 6 ++++++ app/src/main/res/values/strings.xml | 8 ++++++++ app/src/main/res/xml/pref_settings.xml | 16 ++++++++++++++++ 8 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java index aaeeca0..944e3d1 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java @@ -55,6 +55,7 @@ public abstract class NoteDatabase extends RoomDatabase { public static final int VERSION = 8; public static final String DATABASE_NAME = "allthenotes"; + static final Migration MIGRATION_7_8 = new Migration(7, 8) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { @@ -62,9 +63,9 @@ public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL( "CREATE TRIGGER [UpdateTrashTime] AFTER UPDATE ON notes FOR EACH ROW " + - "WHEN NEW.last_modified = OLD.last_modified AND NEW.custom_order = OLD.custom_order AND NEW.in_trash != OLD.in_trash " + + "WHEN NEW.in_trash != OLD.in_trash " + "BEGIN " + - "UPDATE notes SET in_trash_time = (CASE NEW.in_trash WHEN 0 THEN 0 ELSE DateTime('now') END) WHERE _id=NEW._id; " + + "UPDATE notes SET in_trash_time = (CASE NEW.in_trash WHEN 0 THEN 0 ELSE unixepoch('subsec') * 1000 END) WHERE _id=NEW._id; " + "END;" ); } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt index 9a1ef94..e5affbd 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt @@ -57,6 +57,6 @@ interface NoteDao { @Query("SELECT seq + 1 FROM sqlite_sequence WHERE name = :tableName") fun getNextId(tableName: String = "notes"): Int - @Query("SELECT * FROM notes WHERE in_trash = 1 AND in_trash_time <= :timestamp") + @Query("SELECT * FROM notes WHERE in_trash = 1 AND in_trash_time <= :timestamp AND in_trash_time > 0") fun getAllTrashedNotesOlderThan(timestamp: Long): List } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt index b1f195f..8b157f8 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt @@ -32,7 +32,6 @@ data class Note( var in_trash: Int = 0, var last_modified: Long, var custom_order: Int, - var readonly: Int var readonly: Int, var in_trash_time: Long = 0, ) { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt index fb9d7d3..f0a50f4 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -230,8 +230,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte // Delete all trashed notes which are old enough lifecycleScope.launch { - val time = 7 * 24 * 60 * 60 * 1000L - mainActivityViewModel.deleteOldTrashedNotes(time) + PreferenceManager.getDefaultSharedPreferences(this@MainActivity).apply { + if (getBoolean("settings_auto_delete_trashed_notes", false)) { + val time = getString("settings_auto_delete_trashed_notes_time", (7 * 24 * 60 * 60 * 1000L).toString())!!.toLong() + mainActivityViewModel.deleteOldTrashedNotes(time) + } + } } val ith = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 206cdbb..e3a0980 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -178,4 +178,12 @@ Neue Checkliste mit Fertigen Einträge Checkliste durchstreichen Streiche den Beschreibungstext eines Checklisteneintrages durch, sofern dieser abgehakt wurde. + Automatische Löschen im Papierkorb + Falls aktiviert, werden Notizen, die länger als die ausgewählte Zeitspanne im Papierkorb sind, automatisch gelöscht. + Zeitspanne automatisches Löschen + + Einen Tag + Eine Woche + Einen Monat + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 0553e79..8e74a89 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -81,4 +81,10 @@ -1 + + 86400000 + 604800000 + 2592000000 + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e103d6..e04dea8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -202,4 +202,12 @@ This note could not be loaded. Click to save the stored content. Strike checked items in checklist If enabled, checked checklist items will be rendered with a strike-through line. + Autodelete binned note + If enabled, any note longer in the recycle than the specified timespan will be automatically deleted. + Autodelete timespan + + One day + One week + One month + diff --git a/app/src/main/res/xml/pref_settings.xml b/app/src/main/res/xml/pref_settings.xml index 9d869a4..9be138a 100644 --- a/app/src/main/res/xml/pref_settings.xml +++ b/app/src/main/res/xml/pref_settings.xml @@ -16,6 +16,22 @@ android:title="@string/settings_note_trashing_dialog_title" android:summary="@string/settings_note_trashing_dialog_desc" app:iconSpaceReserved="false" /> + + Date: Mon, 13 Apr 2026 19:55:22 +0200 Subject: [PATCH 19/26] [feat] Adds pinned notes. Those are by default in a separate recycler view to keep them at the top of the screen. There is an option to keep them in the same recycler view. --- .../room/NoteDatabase.java | 2 + .../privacyfriendlynotes/room/model/Note.kt | 1 + .../ui/adapter/NoteAdapter.kt | 59 ++++++++ .../ui/main/MainActivity.kt | 138 +++++++++++++++--- .../ui/main/MainActivityViewModel.kt | 5 + app/src/main/res/drawable/ic_pin.xml | 1 + app/src/main/res/layout/content_main.xml | 8 + app/src/main/res/layout/note_item.xml | 14 +- app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/pref_settings.xml | 6 + 11 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 app/src/main/res/drawable/ic_pin.xml diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java index 944e3d1..98ca13a 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java @@ -60,6 +60,7 @@ public abstract class NoteDatabase extends RoomDatabase { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE notes ADD COLUMN in_trash_time INTEGER NOT NULL DEFAULT 0;"); + database.execSQL("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;"); database.execSQL( "CREATE TRIGGER [UpdateTrashTime] AFTER UPDATE ON notes FOR EACH ROW " + @@ -347,6 +348,7 @@ public void migrate(SupportSQLiteDatabase database) { MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, + MIGRATION_7_8, }; private static final RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() { @Override diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt index 8b157f8..5de3d4a 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt @@ -34,6 +34,7 @@ data class Note( var custom_order: Int, var readonly: Int, var in_trash_time: Long = 0, + var pinned: Int = 0, ) { constructor(name: String, content: String, type: Int, category: Int) : this( diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt index 903f3ca..74170d8 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt @@ -24,6 +24,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.view.menu.MenuBuilder import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView @@ -35,6 +36,7 @@ import org.secuso.privacyfriendlynotes.ui.main.MainActivityViewModel import org.secuso.privacyfriendlynotes.ui.util.DarkModeUtil import java.io.File + /** * Adapter that provides a binding for notes * @see org.secuso.privacyfriendlynotes.ui.main.MainActivity @@ -47,11 +49,27 @@ class NoteAdapter( var colorCategory: Boolean, ) : RecyclerView.Adapter() { var startDrag: ((NoteAdapter.NoteHolder) -> Unit)? = null + var setNoteLockState: ((NoteAdapter.NoteHolder, Note, Boolean) -> Unit)? = null + var setNotePinState: ((NoteAdapter.NoteHolder, Note, Boolean) -> Unit)? = null var notes: MutableList = ArrayList() private set var saveContent: ((Note, NoteHolder) -> Unit)? = null private var listener: ((Note, NoteHolder) -> Unit)? = null + + constructor(adapter: NoteAdapter, notes: List? = null) : this( + adapter.activity, + adapter.mainActivityViewModel, + adapter.colorCategory + ) { + startDrag = adapter.startDrag + setNotePinState = adapter.setNotePinState + setNoteLockState = adapter.setNoteLockState + saveContent = adapter.saveContent + listener = adapter.listener + this.notes = (notes ?: adapter.notes).toMutableList() + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteHolder { val itemView = LayoutInflater.from(parent.context) .inflate(R.layout.note_item, parent, false) @@ -177,7 +195,42 @@ class NoteAdapter( holder.textViewDescription.visibility = View.GONE } holder.imageLock.visibility = if (currentNote.readonly > 0) View.VISIBLE else View.GONE + holder.pinHandle.visibility = if (currentNote.pinned > 0) View.VISIBLE else View.GONE holder.dragHandle.visibility = if (mainActivityViewModel.isCustomOrdering()) View.VISIBLE else View.GONE + holder.space.visibility = if (currentNote.readonly > 0 || currentNote.pinned > 0) View.VISIBLE else View.GONE + + holder.itemView.setOnCreateContextMenuListener { menu, v, menuInfo -> + if (currentNote.readonly > 0) { + menu?.add(R.string.action_unlock) + ?.setIcon(R.drawable.lock_open_variant_outline) + ?.setOnMenuItemClickListener { + setNoteLockState?.invoke(holder, currentNote, false) + true + } + } else { + menu?.add(R.string.action_lock) + ?.setIcon(R.drawable.lock_outline) + ?.setOnMenuItemClickListener { + setNoteLockState?.invoke(holder, currentNote, true) + true + } + } + if (currentNote.pinned > 0) { + menu?.add(R.string.action_unpin) + ?.setIcon(R.drawable.ic_pin) + ?.setOnMenuItemClickListener { + setNotePinState?.invoke(holder, currentNote, false) + true + } + } else { + menu?.add( R.string.action_pin) + ?.setIcon(R.drawable.ic_pin) + ?.setOnMenuItemClickListener { + setNotePinState?.invoke(holder, currentNote, true) + true + } + } + } } override fun getItemCount(): Int { @@ -197,8 +250,10 @@ class NoteAdapter( val textViewExtraText: TextView val viewNoteItem: View val dragHandle: View + val pinHandle: View val imageLock: ImageView + val space: View init { textViewTitle = itemView.findViewById(R.id.text_view_title) @@ -208,6 +263,8 @@ class NoteAdapter( viewNoteItem = itemView.findViewById(R.id.note_item) dragHandle = itemView.findViewById(R.id.drag_handle) imageLock = itemView.findViewById(R.id.imageView_lock) + pinHandle = itemView.findViewById(R.id.pin_handle) + space = itemView.findViewById(R.id.note_item_title_space) itemView.setOnClickListener { bindingAdapterPosition.apply { if (listener != null && this != RecyclerView.NO_POSITION) { @@ -216,6 +273,8 @@ class NoteAdapter( } } } + + } fun setOnItemClickListener(listener: (Note, NoteHolder) -> Unit) { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt index f0a50f4..602cd60 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -78,6 +78,7 @@ import java.io.File import java.io.FileOutputStream import java.io.OutputStream import java.util.Collections +import kotlin.math.max /** @@ -91,9 +92,11 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte //New Room variables private val mainActivityViewModel: MainActivityViewModel by lazy { ViewModelProvider(this)[MainActivityViewModel::class.java] } lateinit var adapter: NoteAdapter + var pinnedAdapter: NoteAdapter? = null private val searchView: SearchView by lazy { findViewById(R.id.searchViewFilter) } private lateinit var fab: MainFABFragment private var skipNextNoteFlow = false + private val separatePinnedNotes by lazy { PreferenceManager.getDefaultSharedPreferences(this).getBoolean("settings_pinned_notes_fixed", true) } // A launcher to receive and react to a NoteActivity returning a category // The category is used to set the selectecCategory @@ -222,7 +225,13 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte lifecycleScope.launch { mainActivityViewModel.activeNotes.collect { notes -> if (!skipNextNoteFlow) { - adapter.setNotes(notes) + if (separatePinnedNotes) { + val index = max(0, notes.indexOfFirst { it.pinned == 0 }) + pinnedAdapter!!.setNotes(notes.subList(0, index)) + adapter.setNotes(notes.subList(index, notes.size)) + } else { + adapter.setNotes(notes) + } } skipNextNoteFlow = false } @@ -243,6 +252,10 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte val to = target.bindingAdapterPosition val from = viewHolder.bindingAdapterPosition + // swapping with pinned notes is not possible. + if ((adapter.notes[to].pinned > 0) != (adapter.notes[from].pinned > 0)) { + return false + } // swap custom_orders val temp = adapter.notes[from].custom_order adapter.notes[from].custom_order = adapter.notes[to].custom_order @@ -280,9 +293,92 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte trashNote(note) } } + + override fun isLongPressDragEnabled() = false }) ith.attachToRecyclerView(recyclerView) adapter.startDrag = { holder -> ith.startDrag(holder) } + adapter.setNoteLockState = { holder, note, state -> + note.readonly = if (state) { 1 } else { 0 } + mainActivityViewModel.update(note) + skipNextNoteFlow = true + adapter.notifyItemChanged(holder.bindingAdapterPosition) + } + adapter.setNotePinState = { _, note, state -> + note.pinned = if (state) { 1 } else { 0 } + mainActivityViewModel.update(note) + } + + + if (separatePinnedNotes) { + pinnedAdapter = NoteAdapter(adapter, mutableListOf()) + pinnedAdapter?.setNoteLockState = { holder, note, state -> + note.readonly = if (state) { 1 } else { 0 } + mainActivityViewModel.update(note) + skipNextNoteFlow = true + pinnedAdapter?.notifyItemChanged(holder.bindingAdapterPosition) + } + pinnedAdapter?.setNotePinState = { _, note, state -> + note.pinned = if (state) { 1 } else { 0 } + mainActivityViewModel.update(note) + } + val pinnedIth = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + val to = target.bindingAdapterPosition + val from = viewHolder.bindingAdapterPosition + + // swapping with pinned notes is not possible. + if ((pinnedAdapter!!.notes[to].pinned > 0) != (pinnedAdapter!!.notes[from].pinned > 0)) { + return false + } + // swap custom_orders + val temp = pinnedAdapter!!.notes[from].custom_order + pinnedAdapter!!.notes[from].custom_order = pinnedAdapter!!.notes[to].custom_order + pinnedAdapter!!.notes[to].custom_order = temp + Collections.swap(adapter.notes, from, to) + skipNextNoteFlow = true + mainActivityViewModel.updateAll(listOf(pinnedAdapter!!.notes[from], pinnedAdapter!!.notes[to])) + + pinnedAdapter!!.notifyItemMoved(to, from) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val note = pinnedAdapter!!.getNoteAt(viewHolder.bindingAdapterPosition) + + // Do not delete the note if it is readonly + if (note.readonly > 0) { + pinnedAdapter!!.notifyItemChanged(viewHolder.bindingAdapterPosition) + return + } + + if (PreferenceManager.getDefaultSharedPreferences(this@MainActivity).getBoolean("settings_dialog_on_trashing", false)) { + MaterialAlertDialogBuilder(ContextThemeWrapper(this@MainActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(String.format(getString(R.string.dialog_delete_title), note.name)) + .setMessage(String.format(getString(R.string.dialog_delete_message), note.name)) + .setPositiveButton(R.string.dialog_option_delete) { _, _ -> + pinnedAdapter!!.notifyItemRemoved(viewHolder.bindingAdapterPosition) + trashNote(note) + } + .setNegativeButton(android.R.string.cancel, null) + .setOnDismissListener { pinnedAdapter!!.notifyItemChanged(viewHolder.bindingAdapterPosition) } + .show() + } else { + trashNote(note) + } + } + + override fun isLongPressDragEnabled() = false + }) + pinnedAdapter!!.startDrag = { holder -> pinnedIth.startDrag(holder) } + val pinnedRecyclerView = findViewById(R.id.pinned_notes) + pinnedRecyclerView.layoutManager = LinearLayoutManager(this) + pinnedRecyclerView.setHasFixedSize(true) + pinnedRecyclerView.adapter = pinnedAdapter + pinnedIth.attachToRecyclerView(pinnedRecyclerView) + } + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String): Boolean { mainActivityViewModel.setFilter(newText) @@ -305,26 +401,32 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte /* * Handels when a note is clicked. */ - adapter.setOnItemClickListener { (_id, name, content, type, category, in_trash): Note, _ -> - val launchActivity = - Function, Void?> { activity: Class? -> - val i = Intent(application, activity) - i.putExtra(BaseNoteActivity.EXTRA_ID, _id) - i.putExtra(BaseNoteActivity.EXTRA_TITLE, name) - i.putExtra(BaseNoteActivity.EXTRA_CONTENT, content) - i.putExtra(BaseNoteActivity.EXTRA_CATEGORY, category) - i.putExtra(BaseNoteActivity.EXTRA_ISTRASH, in_trash) - startActivity(i) - null + listOfNotNull(adapter, pinnedAdapter) + .forEach { + it.setOnItemClickListener { (_id, name, content, type, category, in_trash): Note, _ -> + val launchActivity = + Function, Void?> { activity: Class? -> + val i = Intent(application, activity) + i.putExtra(BaseNoteActivity.EXTRA_ID, _id) + i.putExtra(BaseNoteActivity.EXTRA_TITLE, name) + i.putExtra(BaseNoteActivity.EXTRA_CONTENT, content) + i.putExtra(BaseNoteActivity.EXTRA_CATEGORY, category) + i.putExtra(BaseNoteActivity.EXTRA_ISTRASH, in_trash) + startActivity(i) + null + } + when (type) { + DbContract.NoteEntry.TYPE_TEXT -> launchActivity.apply(TextNoteActivity::class.java) + DbContract.NoteEntry.TYPE_AUDIO -> launchActivity.apply(AudioNoteActivity::class.java) + DbContract.NoteEntry.TYPE_SKETCH -> launchActivity.apply(SketchActivity::class.java) + DbContract.NoteEntry.TYPE_CHECKLIST -> launchActivity.apply( + ChecklistNoteActivity::class.java + ) } - when (type) { - DbContract.NoteEntry.TYPE_TEXT -> launchActivity.apply(TextNoteActivity::class.java) - DbContract.NoteEntry.TYPE_AUDIO -> launchActivity.apply(AudioNoteActivity::class.java) - DbContract.NoteEntry.TYPE_SKETCH -> launchActivity.apply(SketchActivity::class.java) - DbContract.NoteEntry.TYPE_CHECKLIST -> launchActivity.apply(ChecklistNoteActivity::class.java) + fab.close() } - fab.close() } + val theme = PreferenceManager.getDefaultSharedPreferences(this).getString("settings_day_night_theme", "-1") AppCompatDelegate.setDefaultNightMode(theme!!.toInt()) } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt index 33942ed..3ddf253 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt @@ -82,6 +82,7 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica .filterCategories() .filterNotes() .sortNotes() + .sortPinned() val categories: Flow> = repository.categoryDao().allCategories private val filesDir: File = application.filesDir private val resources: Resources = application.resources @@ -198,6 +199,10 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica return this.map { it.sortedWith(ordering.comparator()).apply { return@map if (reversed.value) this.reversed() else this } } } + private fun Flow>.sortPinned(): Flow> { + return this.map { it.sortedWith { a,b -> -a.pinned.compareTo(b.pinned) } } + } + private fun Flow>.filterCategories(): Flow> { return this.map { it.filter { note -> diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml new file mode 100644 index 0000000..505db57 --- /dev/null +++ b/app/src/main/res/drawable/ic_pin.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 0d2b273..189858a 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -23,6 +23,14 @@ app:queryBackground="@color/transparent" android:imeOptions="flagNoExtractUi" /> + + + + + + Kategorie In Checkliste umwandeln In Textnotiz umwandeln + Anheften + Lösen Autoren: Version Über @@ -186,4 +188,6 @@ Eine Woche Einen Monat + Fixe angehefete Notizen + Falls aktiviert, dann werden angeheftete Notizen in einem separaten Bereich am über den restlichen Notizen angezeigt. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e04dea8..41d8ce0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,6 +56,8 @@ Redo Convert to checklist Convert to text note + Pin + Unpin Name Note New item @@ -210,4 +212,6 @@ One week One month + Fixed pinned notes + If enabled, pinned notes will be displayed in a separate area fixed at the top of the screen. diff --git a/app/src/main/res/xml/pref_settings.xml b/app/src/main/res/xml/pref_settings.xml index 9be138a..0e1b8d6 100644 --- a/app/src/main/res/xml/pref_settings.xml +++ b/app/src/main/res/xml/pref_settings.xml @@ -104,6 +104,12 @@ android:entries="@array/font_size_entries" android:entryValues="@array/font_size_values" app:iconSpaceReserved="false" /> + Date: Mon, 13 Apr 2026 20:14:08 +0200 Subject: [PATCH 20/26] [fix] fixed pinned notes may not take up full screen height. --- app/src/main/res/layout/content_main.xml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 189858a..cc4086b 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -8,10 +8,9 @@ tools:context="org.secuso.privacyfriendlynotes.ui.main.MainActivity" tools:showIn="@layout/app_bar_main"> - + android:layout_height="match_parent"> + android:imeOptions="flagNoExtractUi" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + android:clipToPadding="false" + app:layout_constraintTop_toBottomOf="@id/searchViewFilter" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintHeight_max="200dp" /> + android:clipToPadding="false" + app:layout_constraintTop_toBottomOf="@id/pinned_notes" + app:layout_constraintStart_toStartOf="parent" /> - + From acf940ed400f699f90e0fee6ee1f87f7a808cbad Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 14 Apr 2026 15:59:17 +0200 Subject: [PATCH 21/26] [feat] Allows marking notes as done. Adds option to delete all done notes. Also shows done / all notes number per category in menu. --- .../room/NoteDatabase.java | 13 +++++- .../room/dao/CategoryDao.kt | 6 +++ .../room/model/Category.kt | 11 ++++- .../privacyfriendlynotes/room/model/Note.kt | 1 + .../ui/adapter/NoteAdapter.kt | 41 ++++++++++++++++--- .../ui/main/MainActivity.kt | 32 +++++++++++++-- .../ui/main/MainActivityViewModel.kt | 2 + app/src/main/res/layout/note_item.xml | 39 ++++++++++-------- app/src/main/res/menu/main.xml | 6 +++ app/src/main/res/values-de/strings.xml | 4 ++ app/src/main/res/values/strings.xml | 4 ++ 11 files changed, 132 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java index 98ca13a..93455a9 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java @@ -53,14 +53,24 @@ ) public abstract class NoteDatabase extends RoomDatabase { - public static final int VERSION = 8; + public static final int VERSION = 9; public static final String DATABASE_NAME = "allthenotes"; + static final Migration MIGRATION_8_9 = new Migration(8, 9) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE notes DROP COLUMN is_done;"); + + database.execSQL("ALTER TABLE notes ADD COLUMN is_done INTEGER NOT NULL DEFAULT 0;"); + } + }; + static final Migration MIGRATION_7_8 = new Migration(7, 8) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE notes ADD COLUMN in_trash_time INTEGER NOT NULL DEFAULT 0;"); database.execSQL("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;"); + database.execSQL("ALTER TABLE notes ADD COLUMN is_done INTEGER NOT NULL DEFAULT 0;"); database.execSQL( "CREATE TRIGGER [UpdateTrashTime] AFTER UPDATE ON notes FOR EACH ROW " + @@ -349,6 +359,7 @@ public void migrate(SupportSQLiteDatabase database) { MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, + MIGRATION_8_9, }; private static final RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() { @Override diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt index ec76334..b17ab0f 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt @@ -22,6 +22,7 @@ import androidx.room.Query import androidx.room.Update import kotlinx.coroutines.flow.Flow import org.secuso.privacyfriendlynotes.room.model.Category +import org.secuso.privacyfriendlynotes.room.model.CategoryWithCompleteInformation /** * Data Access Object for categories that define the interactions with the database @@ -43,6 +44,11 @@ interface CategoryDao { @get:Query("SELECT * FROM categories GROUP BY name") val allCategories: Flow> + @get:Query("SELECT '' as name, -1 as _id, COUNT(CASE WHEN is_done > 0 THEN 1 END) AS done, COUNT(*) AS _all FROM notes " + + "UNION " + + "SELECT categories.name as name, categories._id, COUNT(CASE WHEN notes.is_done > 0 THEN 1 END) AS done, COUNT(*) AS _all FROM categories INNER JOIN notes ON categories._id == notes.category GROUP BY categories.name" + ) + val allCategoriesWithDoneInformation: Flow> @get:Query("SELECT * FROM categories GROUP BY name") val allCategoriesSync: List diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt index 137807e..96df67e 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt @@ -13,6 +13,8 @@ */ package org.secuso.privacyfriendlynotes.room.model +import androidx.room.ColumnInfo +import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey @@ -40,4 +42,11 @@ data class Category( color = color ) -} \ No newline at end of file +} + +data class CategoryWithCompleteInformation( + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "_id") val _id: Int, + @ColumnInfo(name = "done") val done: Int, + @ColumnInfo(name = "_all") val all: Int, +) \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt index 5de3d4a..3b97869 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt @@ -35,6 +35,7 @@ data class Note( var readonly: Int, var in_trash_time: Long = 0, var pinned: Int = 0, + var is_done: Int = 0, ) { constructor(name: String, content: String, type: Int, category: Int) : this( diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt index 74170d8..587a551 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt @@ -15,12 +15,14 @@ package org.secuso.privacyfriendlynotes.ui.adapter import android.app.Activity import android.graphics.Color +import android.graphics.Paint import android.text.Html import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CheckBox import android.widget.ImageView import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources @@ -51,6 +53,7 @@ class NoteAdapter( var startDrag: ((NoteAdapter.NoteHolder) -> Unit)? = null var setNoteLockState: ((NoteAdapter.NoteHolder, Note, Boolean) -> Unit)? = null var setNotePinState: ((NoteAdapter.NoteHolder, Note, Boolean) -> Unit)? = null + var setNoteCheckedState: ((NoteAdapter.NoteHolder, Note, Boolean) -> Unit)? = null var notes: MutableList = ArrayList() private set @@ -197,21 +200,34 @@ class NoteAdapter( holder.imageLock.visibility = if (currentNote.readonly > 0) View.VISIBLE else View.GONE holder.pinHandle.visibility = if (currentNote.pinned > 0) View.VISIBLE else View.GONE holder.dragHandle.visibility = if (mainActivityViewModel.isCustomOrdering()) View.VISIBLE else View.GONE - holder.space.visibility = if (currentNote.readonly > 0 || currentNote.pinned > 0) View.VISIBLE else View.GONE + holder.space.visibility = if (currentNote.readonly > 0 || currentNote.pinned > 0 || currentNote.is_done > 0) View.VISIBLE else View.GONE + holder.checkedHandle.visibility = if (currentNote.is_done > 0) View.VISIBLE else View.GONE + holder.checkedHandle.setOnClickListener { setNoteCheckedState?.invoke(holder, notes[holder.bindingAdapterPosition], currentNote.is_done == 0) } + + val strikeThrough = PreferenceManager.getDefaultSharedPreferences(holder.itemView.context).getBoolean("settings_checklist_strike_items", true) + listOf(holder.textViewTitle, holder.textViewDescription).forEach { + it.apply { + paintFlags = if (currentNote.is_done > 0 && strikeThrough) { + paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + } else { + paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + } + } + } holder.itemView.setOnCreateContextMenuListener { menu, v, menuInfo -> if (currentNote.readonly > 0) { menu?.add(R.string.action_unlock) ?.setIcon(R.drawable.lock_open_variant_outline) ?.setOnMenuItemClickListener { - setNoteLockState?.invoke(holder, currentNote, false) + setNoteLockState?.invoke(holder, notes[holder.bindingAdapterPosition], false) true } } else { menu?.add(R.string.action_lock) ?.setIcon(R.drawable.lock_outline) ?.setOnMenuItemClickListener { - setNoteLockState?.invoke(holder, currentNote, true) + setNoteLockState?.invoke(holder, notes[holder.bindingAdapterPosition], true) true } } @@ -219,14 +235,27 @@ class NoteAdapter( menu?.add(R.string.action_unpin) ?.setIcon(R.drawable.ic_pin) ?.setOnMenuItemClickListener { - setNotePinState?.invoke(holder, currentNote, false) + setNotePinState?.invoke(holder, notes[holder.bindingAdapterPosition], false) true } } else { menu?.add( R.string.action_pin) ?.setIcon(R.drawable.ic_pin) ?.setOnMenuItemClickListener { - setNotePinState?.invoke(holder, currentNote, true) + setNotePinState?.invoke(holder, notes[holder.bindingAdapterPosition], true) + true + } + } + if (currentNote.is_done > 0) { + menu?.add(R.string.action_not_done) + ?.setOnMenuItemClickListener { + setNoteCheckedState?.invoke(holder, notes[holder.bindingAdapterPosition], false) + true + } + } else { + menu?.add( R.string.action_done) + ?.setOnMenuItemClickListener { + setNoteCheckedState?.invoke(holder, notes[holder.bindingAdapterPosition], true) true } } @@ -251,6 +280,7 @@ class NoteAdapter( val viewNoteItem: View val dragHandle: View val pinHandle: View + val checkedHandle: View val imageLock: ImageView val space: View @@ -265,6 +295,7 @@ class NoteAdapter( imageLock = itemView.findViewById(R.id.imageView_lock) pinHandle = itemView.findViewById(R.id.pin_handle) space = itemView.findViewById(R.id.note_item_title_space) + checkedHandle = itemView.findViewById(R.id.done_handle) itemView.setOnClickListener { bindingAdapterPosition.apply { if (listener != null && this != RecyclerView.NO_POSITION) { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt index 602cd60..a7109ee 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -308,6 +308,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte note.pinned = if (state) { 1 } else { 0 } mainActivityViewModel.update(note) } + adapter.setNoteCheckedState = { holder, note, state -> + note.is_done = if (state) 1 else 0 + skipNextNoteFlow = true + mainActivityViewModel.update(note) + adapter.notifyItemChanged(holder.bindingAdapterPosition) + } if (separatePinnedNotes) { @@ -322,6 +328,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte note.pinned = if (state) { 1 } else { 0 } mainActivityViewModel.update(note) } + pinnedAdapter?.setNoteCheckedState = { holder, note, state -> + note.is_done = if (state) 1 else 0 + skipNextNoteFlow = true + mainActivityViewModel.update(note) + pinnedAdapter?.notifyItemChanged(holder.bindingAdapterPosition) + } val pinnedIth = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { val to = target.bindingAdapterPosition @@ -488,6 +500,15 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte dialog.chooseSortingOption() } else if (id == R.id.action_export_all) { exportAllNotes() + } else if (id == R.id.action_delete_all_finished) { + val notes = listOfNotNull(adapter, pinnedAdapter).flatMap { + it.notes.filter { note -> note.is_done > 0 } + }.map { + it.in_trash = 1 + it + } + mainActivityViewModel.updateAll(notes) + Toast.makeText(this@MainActivity, getString(R.string.toast_deleted_multiple), Toast.LENGTH_SHORT).show() } return super.onOptionsItemSelected(item) } @@ -544,10 +565,13 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte //Get the rest from the database lifecycleScope.launch { - mainActivityViewModel.categories.collect { - navMenu.add(R.id.drawer_group2, 0, Menu.NONE, getString(R.string.default_category)).setIcon(R.drawable.ic_label_black_24dp) - for ((id, name) in it) { - navMenu.add(R.id.drawer_group2, id, Menu.NONE, name).setIcon(R.drawable.ic_label_black_24dp) + mainActivityViewModel.categoriesWithDoneInformation.collect { + navMenu.removeItem(0) + val (name, _id, done, all) = it.first() + navMenu.add(R.id.drawer_group2, 0, Menu.NONE, String.format("%s \t %s/%s", getString(R.string.default_category), done, all)).setIcon(R.drawable.ic_label_black_24dp) + for ((name, _id, done, all) in it.subList(1, it.size)) { + navMenu.removeItem(_id) + navMenu.add(R.id.drawer_group2, _id, Menu.NONE, String.format("%s \t %s/%s", name, done, all)).setIcon(R.drawable.ic_label_black_24dp) } } } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt index 3ddf253..e855989 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt @@ -35,6 +35,7 @@ import org.secuso.privacyfriendlynotes.preference.PreferenceKeys import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.NoteDatabase import org.secuso.privacyfriendlynotes.room.model.Category +import org.secuso.privacyfriendlynotes.room.model.CategoryWithCompleteInformation import org.secuso.privacyfriendlynotes.room.model.Note import org.secuso.privacyfriendlynotes.ui.notes.AudioNoteActivity import org.secuso.privacyfriendlynotes.ui.notes.ChecklistNoteActivity @@ -84,6 +85,7 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica .sortNotes() .sortPinned() val categories: Flow> = repository.categoryDao().allCategories + val categoriesWithDoneInformation: Flow> = repository.categoryDao().allCategoriesWithDoneInformation private val filesDir: File = application.filesDir private val resources: Resources = application.resources diff --git a/app/src/main/res/layout/note_item.xml b/app/src/main/res/layout/note_item.xml index 9bd6f45..b023ab1 100644 --- a/app/src/main/res/layout/note_item.xml +++ b/app/src/main/res/layout/note_item.xml @@ -17,7 +17,7 @@ android:orientation="horizontal"> + + + + + + - - - - + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 677c441..de567eb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -85,6 +85,9 @@ In Textnotiz umwandeln Anheften Lösen + Erledigt + Nicht erledigt + Lösche alle erledigten Autoren: Version Über @@ -171,6 +174,7 @@ Die geöffnete Datei ist zu groß Die Datei hat zu viele Zeichen, lade nur bis zum Limit. Notiz gelöscht + Notizen gelöscht Textnotiz Zeichenlimit für importierte Textnotizen Dateigrößenlimit für importierte Textnotizen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41d8ce0..842f629 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,9 @@ Convert to text note Pin Unpin + Mark as done + Mark as unfinished + Delete all done Name Note New item @@ -76,6 +79,7 @@ The opened file is too large. The file has too many characters. Only loading until limit reached. Note deleted + Notes deleted Delete %1s? Are you sure you want to delete %1s? Are you sure you want to delete %1s? All notes in the category will be deleted! From ded29f0a34a43dfeef6d3d3b9e7dbf290f4dad01 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 14 Apr 2026 16:03:17 +0200 Subject: [PATCH 22/26] [fix] #236 trailing new lines in text note. --- .../privacyfriendlynotes/ui/notes/TextNoteActivity.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index aa80948..9db404f 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -232,7 +232,11 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { } override fun onNoteLoadedFromDB(note: Note) { - etContent.setText(HtmlCompat.fromHtml(note.content, HtmlCompat.FROM_HTML_MODE_LEGACY, htmlImageGetter, null)) + etContent.setText( + note.content.parseAsHtml( + HtmlCompat.FROM_HTML_MODE_LEGACY, + htmlImageGetter + ).trimEnd(' ', '\n')) etContent.setSelection(lastCursorPosition.coerceIn(0, etContent.text.length)) oldText = etContent.text.toString() } @@ -619,7 +623,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { return if (name.isEmpty() && etContent.text.toString().isEmpty()) { ActionResult(false, null) } else { - ActionResult(true, Note(name, Html.toHtml(etContent.text), DbContract.NoteEntry.TYPE_TEXT, category)) + ActionResult(true, Note(name, Html.toHtml(etContent.text).trimEnd(' ', '\n'), DbContract.NoteEntry.TYPE_TEXT, category)) } } From d07fdef81c5bb83168662c6149b02efc8f6833d6 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 14 Apr 2026 16:09:28 +0200 Subject: [PATCH 23/26] [fix] could not change to default category in note. Closes #237. --- .../org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt index b5c4739..af52768 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt @@ -158,6 +158,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli val catName = catSelection.text.toString() if (catName == getString(R.string.default_category)) { currentCat = 0 + hasChanged = hasChanged or (savedCat != 0) return@setOnDismissListener } currentCat = -1 From f08acf011f7c3110ee87d6434ec30ae07732f834 Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 14 Apr 2026 20:23:01 +0200 Subject: [PATCH 24/26] [feat] Adds option to change category of many notes at once. --- .../ui/adapter/NoteAdapter.kt | 30 ++++++++++- .../ui/main/MainActivity.kt | 54 +++++++++++++++++++ .../ui/main/MainActivityViewModel.kt | 3 ++ app/src/main/res/layout/note_item.xml | 6 +++ app/src/main/res/menu/main.xml | 6 +++ .../main/res/menu/main_change_category.xml | 10 ++++ app/src/main/res/values-de/strings.xml | 4 ++ app/src/main/res/values-night/styles.xml | 9 ++++ app/src/main/res/values/strings.xml | 4 ++ app/src/main/res/values/styles.xml | 12 +++++ 10 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/menu/main_change_category.xml diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt index 587a551..69b9687 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt @@ -13,6 +13,7 @@ */ package org.secuso.privacyfriendlynotes.ui.adapter +import android.annotation.SuppressLint import android.app.Activity import android.graphics.Color import android.graphics.Paint @@ -60,6 +61,18 @@ class NoteAdapter( var saveContent: ((Note, NoteHolder) -> Unit)? = null private var listener: ((Note, NoteHolder) -> Unit)? = null + private val _selection: MutableSet = mutableSetOf() + val selection: List + get() = _selection.map { notes[it] }.toList() + + var selectionMode: Boolean = false + @SuppressLint("NotifyDataSetChanged") + set(value) { + field = value + _selection.clear() + notifyDataSetChanged() + } + constructor(adapter: NoteAdapter, notes: List? = null) : this( adapter.activity, adapter.mainActivityViewModel, @@ -199,7 +212,20 @@ class NoteAdapter( } holder.imageLock.visibility = if (currentNote.readonly > 0) View.VISIBLE else View.GONE holder.pinHandle.visibility = if (currentNote.pinned > 0) View.VISIBLE else View.GONE - holder.dragHandle.visibility = if (mainActivityViewModel.isCustomOrdering()) View.VISIBLE else View.GONE + if (selectionMode) { + holder.dragHandle.visibility = View.GONE + holder.selectionCheckbox.visibility = View.VISIBLE + holder.selectionCheckbox.setOnClickListener { + if (_selection.contains(holder.bindingAdapterPosition)) { + _selection.remove(holder.bindingAdapterPosition) + } else { + _selection.add(holder.bindingAdapterPosition) + } + } + } else { + holder.dragHandle.visibility = if (mainActivityViewModel.isCustomOrdering()) View.VISIBLE else View.GONE + holder.selectionCheckbox.visibility = View.GONE + } holder.space.visibility = if (currentNote.readonly > 0 || currentNote.pinned > 0 || currentNote.is_done > 0) View.VISIBLE else View.GONE holder.checkedHandle.visibility = if (currentNote.is_done > 0) View.VISIBLE else View.GONE holder.checkedHandle.setOnClickListener { setNoteCheckedState?.invoke(holder, notes[holder.bindingAdapterPosition], currentNote.is_done == 0) } @@ -284,6 +310,7 @@ class NoteAdapter( val imageLock: ImageView val space: View + val selectionCheckbox: CheckBox init { textViewTitle = itemView.findViewById(R.id.text_view_title) @@ -296,6 +323,7 @@ class NoteAdapter( pinHandle = itemView.findViewById(R.id.pin_handle) space = itemView.findViewById(R.id.note_item_title_space) checkedHandle = itemView.findViewById(R.id.done_handle) + selectionCheckbox = itemView.findViewById(R.id.checkbox) itemView.setOnClickListener { bindingAdapterPosition.apply { if (listener != null && this != RecyclerView.NO_POSITION) { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt index a7109ee..6769525 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -22,6 +22,7 @@ import android.preference.PreferenceManager import android.text.Html import android.util.Log import android.util.TypedValue +import android.view.ActionMode import android.view.ContextThemeWrapper import android.view.Menu import android.view.MenuInflater @@ -59,6 +60,7 @@ import kotlinx.coroutines.runBlocking import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.model.SortingOrder import org.secuso.privacyfriendlynotes.room.DbContract +import org.secuso.privacyfriendlynotes.room.model.Category import org.secuso.privacyfriendlynotes.room.model.Note import org.secuso.privacyfriendlynotes.ui.AboutActivity import org.secuso.privacyfriendlynotes.ui.HelpActivity @@ -98,6 +100,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte private var skipNextNoteFlow = false private val separatePinnedNotes by lazy { PreferenceManager.getDefaultSharedPreferences(this).getBoolean("settings_pinned_notes_fixed", true) } + val defaultCategory by lazy { Category(0,resources.getString(R.string.default_category), null) } + // A launcher to receive and react to a NoteActivity returning a category // The category is used to set the selectecCategory private var setCategoryResultAfter = registerForActivityResult( @@ -165,6 +169,54 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } } + private val actionModeChangeCategory = object : ActionMode.Callback { + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return if (item.itemId == R.id.action_mode_select_category) { + val categories = mainActivityViewModel.categoriesSync.toMutableList().apply { + add(0, defaultCategory) + } + var selectedCategory: Category? = null + MaterialAlertDialogBuilder(ContextThemeWrapper(this@MainActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(R.string.dialog_change_multiple_category_title) + .setSingleChoiceItems(categories.map { it.name }.toTypedArray(), -1) { _, which -> + selectedCategory = categories[which] + } + .setPositiveButton(R.string.dialog_change_multiple_category_btn) { _, _ -> + mainActivityViewModel.updateAll( + adapter.selection.map { + it.category = selectedCategory!!._id + it + } + ) + adapter.selectionMode = false + mode.finish() + } + .setNegativeButton(android.R.string.cancel) { _,_ -> + adapter.selectionMode = false + mode.finish() + } + .show() + true + } else { + false + } + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.main_change_category, menu) + return true + } + + override fun onDestroyActionMode(mode: ActionMode?) { + adapter.selectionMode = false + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + adapter.selectionMode = true + return true + } + } + override fun onCreate(savedInstanceState: Bundle?) { supportFragmentManager.fragmentFactory = object : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { @@ -509,6 +561,8 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } mainActivityViewModel.updateAll(notes) Toast.makeText(this@MainActivity, getString(R.string.toast_deleted_multiple), Toast.LENGTH_SHORT).show() + } else if (id == R.id.action_change_multiple_category) { + startActionMode(actionModeChangeCategory) } return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt index e855989..c7b38eb 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt @@ -85,6 +85,9 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica .sortNotes() .sortPinned() val categories: Flow> = repository.categoryDao().allCategories + val categoriesSync: List + get() = repository.categoryDao().allCategoriesSync + val categoriesWithDoneInformation: Flow> = repository.categoryDao().allCategoriesWithDoneInformation private val filesDir: File = application.filesDir private val resources: Resources = application.resources diff --git a/app/src/main/res/layout/note_item.xml b/app/src/main/res/layout/note_item.xml index b023ab1..24f6d4d 100644 --- a/app/src/main/res/layout/note_item.xml +++ b/app/src/main/res/layout/note_item.xml @@ -92,6 +92,12 @@ app:tint="?attr/colorIconFill" android:src="@drawable/ic_baseline_drag_indicator_icon_24dp"/> + + \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 6af42db..f2dcfd0 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -19,4 +19,10 @@ android:title="@string/action_delete_all_finished" app:showAsAction="never" /> + diff --git a/app/src/main/res/menu/main_change_category.xml b/app/src/main/res/menu/main_change_category.xml new file mode 100644 index 0000000..bfbeaee --- /dev/null +++ b/app/src/main/res/menu/main_change_category.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index de567eb..a8c7fdf 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -32,6 +32,8 @@ In den Papierkorb verschieben Kategorie erstellen Erstellen + Kategorie auswählen + Auswählen Kategorie erstellen Ändere Kategorienname Audio @@ -88,6 +90,8 @@ Erledigt Nicht erledigt Lösche alle erledigten + Kategorie auswählen + Kategorie ändern Autoren: Version Über diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 0400390..6895804 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -6,6 +6,8 @@ @color/colorSurfaceSecondaryDark @color/colorBackgroundDark @color/colorBackgroundDark + @color/colorBackgroundDark + true @color/md_theme_dark_primary @color/md_theme_dark_onPrimary @@ -49,6 +51,8 @@ @drawable/arrow_back @style/ThemeOverlay.App.Preference @style/AppTheme.ActionBar.OverflowMenu + @style/AppTheme.ActionMode + @drawable/arrow_back + + + + + + From 53441c02a978fc4bbb5b1100c81d695b162e7aca Mon Sep 17 00:00:00 2001 From: Patrick Schneider Date: Tue, 14 Apr 2026 21:11:36 +0200 Subject: [PATCH 25/26] [feat] Adds option to sort checklist items. --- .../ui/adapter/ChecklistAdapter.kt | 41 ++++++++++++++++--- .../ui/notes/ChecklistNoteActivity.kt | 14 +++++++ .../ic_sort_by_alpha_asc_icon_24dp.xml | 15 +++++++ .../ic_sort_by_alpha_desc_icon_24dp.xml | 16 ++++++++ app/src/main/res/menu/activity_checklist.xml | 7 ++++ 5 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/drawable/ic_sort_by_alpha_asc_icon_24dp.xml create mode 100644 app/src/main/res/drawable/ic_sort_by_alpha_desc_icon_24dp.xml diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt index 6917486..70e31f7 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt @@ -17,6 +17,7 @@ import android.annotation.SuppressLint import android.graphics.Paint import android.text.Editable import android.text.TextWatcher +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -38,6 +39,19 @@ class ChecklistAdapter( private val startDrag: (ItemHolder) -> Unit, ) : RecyclerView.Adapter() { + var sortingOption: SortingOption = SortingOption.NONE + set(value) { + items.sortWith { a, b -> + when (value) { + SortingOption.ASCENDING -> a.name.compareTo(b.name) + SortingOption.DESCENDING -> b.name.compareTo(a.name) + SortingOption.NONE -> 1 + } + } + Log.d("Sorting", "$value") + notifyDataSetChanged() + field = value + } private var items: MutableList = mutableListOf() var hasChanged = false private set @@ -80,9 +94,11 @@ class ChecklistAdapter( val strikeThrough = PreferenceManager.getDefaultSharedPreferences(holder.itemView.context).getBoolean("settings_checklist_strike_items", true) holder.textView.text = item holder.checkbox.isChecked = checked - holder.dragHandle.setOnTouchListener { v, _ -> - startDrag(holder) - v.performClick() + if (sortingOption == SortingOption.NONE) { + holder.dragHandle.setOnTouchListener { v, _ -> + startDrag(holder) + v.performClick() + } } holder.checkbox.setOnClickListener { _ -> items[holder.bindingAdapterPosition].state = holder.checkbox.isChecked @@ -130,8 +146,17 @@ class ChecklistAdapter( } fun addItem(item: String) { - this.items.add(ChecklistItem(false, item)) - notifyItemInserted(items.size - 1) + var index = when (sortingOption) { + SortingOption.NONE -> items.size + SortingOption.ASCENDING -> items.indexOfFirst { it.name >= item } + SortingOption.DESCENDING -> items.indexOfFirst { it.name <= item } + } + // item not found -> index = -1 -> should be at end + if (index < 0) { + index = items.size + } + this.items.add(index, ChecklistItem(false, item)) + notifyItemInserted(index) hasChanged = true } @@ -150,4 +175,10 @@ class ChecklistAdapter( val checkbox: MaterialCheckBox = itemView.findViewById(R.id.item_checkbox) val dragHandle: View = itemView.findViewById(R.id.drag_handle) } + + enum class SortingOption { + ASCENDING, + DESCENDING, + NONE + } } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt index 04ed274..161e02c 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt @@ -123,6 +123,20 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { + R.id.action_sort_alphabetical -> { + val next = when (adapter.sortingOption) { + ChecklistAdapter.SortingOption.NONE -> ChecklistAdapter.SortingOption.ASCENDING + ChecklistAdapter.SortingOption.ASCENDING -> ChecklistAdapter.SortingOption.DESCENDING + ChecklistAdapter.SortingOption.DESCENDING -> ChecklistAdapter.SortingOption.NONE + } + val icon = when (next) { + ChecklistAdapter.SortingOption.NONE -> R.drawable.ic_sort_by_alpha_icon_24dp + ChecklistAdapter.SortingOption.ASCENDING -> R.drawable.ic_sort_by_alpha_asc_icon_24dp + ChecklistAdapter.SortingOption.DESCENDING -> R.drawable.ic_sort_by_alpha_desc_icon_24dp + } + adapter.sortingOption = next + item.setIcon(icon) + } R.id.action_convert_to_note -> { MaterialAlertDialogBuilder(ContextThemeWrapper(this@ChecklistNoteActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) .setTitle(R.string.dialog_convert_to_text_title) diff --git a/app/src/main/res/drawable/ic_sort_by_alpha_asc_icon_24dp.xml b/app/src/main/res/drawable/ic_sort_by_alpha_asc_icon_24dp.xml new file mode 100644 index 0000000..92824d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_by_alpha_asc_icon_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_sort_by_alpha_desc_icon_24dp.xml b/app/src/main/res/drawable/ic_sort_by_alpha_desc_icon_24dp.xml new file mode 100644 index 0000000..ce9b5ef --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_by_alpha_desc_icon_24dp.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/menu/activity_checklist.xml b/app/src/main/res/menu/activity_checklist.xml index 67448b7..96c4ed7 100644 --- a/app/src/main/res/menu/activity_checklist.xml +++ b/app/src/main/res/menu/activity_checklist.xml @@ -1,6 +1,13 @@ + Date: Tue, 14 Apr 2026 21:16:13 +0200 Subject: [PATCH 26/26] [fix] lint. --- .../org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt index 6769525..910711b 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -257,7 +257,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte //Fill from Room database val recyclerView = findViewById(R.id.recycler_view) recyclerView.layoutManager = LinearLayoutManager(this) - recyclerView.setHasFixedSize(true) adapter = NoteAdapter( this, mainActivityViewModel, @@ -438,7 +437,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte pinnedAdapter!!.startDrag = { holder -> pinnedIth.startDrag(holder) } val pinnedRecyclerView = findViewById(R.id.pinned_notes) pinnedRecyclerView.layoutManager = LinearLayoutManager(this) - pinnedRecyclerView.setHasFixedSize(true) pinnedRecyclerView.adapter = pinnedAdapter pinnedIth.attachToRecyclerView(pinnedRecyclerView) }