diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7cde17e..5d893f93b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Option to make a note read-only ([#69]) ## [1.7.0] - 2026-01-30 ### Added @@ -104,6 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release [#59]: https://github.com/FossifyOrg/Notes/issues/59 +[#69]: https://github.com/FossifyOrg/Notes/issues/69 [#81]: https://github.com/FossifyOrg/Notes/issues/81 [#83]: https://github.com/FossifyOrg/Notes/issues/83 [#96]: https://github.com/FossifyOrg/Notes/issues/96 diff --git a/app/schemas/org.fossify.notes.databases.NotesDatabase/5.json b/app/schemas/org.fossify.notes.databases.NotesDatabase/5.json new file mode 100644 index 000000000..2f01a6801 --- /dev/null +++ b/app/schemas/org.fossify.notes.databases.NotesDatabase/5.json @@ -0,0 +1,142 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "6d2c2b2e1c69d9d296b9c6c621f66033", + "entities": [ + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `value` TEXT NOT NULL, `type` INTEGER NOT NULL, `path` TEXT NOT NULL, `protection_type` INTEGER NOT NULL, `protection_hash` TEXT NOT NULL, `is_read_only` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protectionType", + "columnName": "protection_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "protectionHash", + "columnName": "protection_hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isReadOnly", + "columnName": "is_read_only", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_notes_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_notes_id` ON `${TABLE_NAME}` (`id`)" + } + ] + }, + { + "tableName": "widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `widget_id` INTEGER NOT NULL, `note_id` INTEGER NOT NULL, `widget_bg_color` INTEGER NOT NULL, `widget_text_color` INTEGER NOT NULL, `widget_show_title` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "noteId", + "columnName": "note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetBgColor", + "columnName": "widget_bg_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetTextColor", + "columnName": "widget_text_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetShowTitle", + "columnName": "widget_show_title", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_widgets_widget_id", + "unique": true, + "columnNames": [ + "widget_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_widgets_widget_id` ON `${TABLE_NAME}` (`widget_id`)" + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6d2c2b2e1c69d9d296b9c6c621f66033')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/fossify/notes/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/notes/activities/MainActivity.kt index 725778452..9cbca7413 100644 --- a/app/src/main/kotlin/org/fossify/notes/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/notes/activities/MainActivity.kt @@ -254,33 +254,37 @@ class MainActivity : SimpleActivity() { binding.mainToolbar.menu.apply { findItem(R.id.undo).apply { - isVisible = showUndoButton && mCurrentNote.type == NoteType.TYPE_TEXT + isVisible = showUndoButton && ::mCurrentNote.isInitialized && !mCurrentNote.isReadOnly && mCurrentNote.type == NoteType.TYPE_TEXT icon?.alpha = if (isEnabled) 255 else 127 } findItem(R.id.redo).apply { - isVisible = showRedoButton && mCurrentNote.type == NoteType.TYPE_TEXT + isVisible = showRedoButton && ::mCurrentNote.isInitialized && !mCurrentNote.isReadOnly && mCurrentNote.type == NoteType.TYPE_TEXT icon?.alpha = if (isEnabled) 255 else 127 } findItem(R.id.rename_note).isVisible = multipleNotesExist findItem(R.id.open_note).isVisible = multipleNotesExist findItem(R.id.open_search).isVisible = !isCurrentItemChecklist - findItem(R.id.remove_done_items).isVisible = isCurrentItemChecklist - findItem(R.id.uncheck_all_items).isVisible = isCurrentItemChecklist - findItem(R.id.sort_checklist).isVisible = isCurrentItemChecklist + findItem(R.id.remove_done_items).isVisible = isCurrentItemChecklist && ::mCurrentNote.isInitialized && !mCurrentNote.isReadOnly + findItem(R.id.uncheck_all_items).isVisible = isCurrentItemChecklist && ::mCurrentNote.isInitialized && !mCurrentNote.isReadOnly + findItem(R.id.sort_checklist).isVisible = isCurrentItemChecklist && ::mCurrentNote.isInitialized && !mCurrentNote.isReadOnly findItem(R.id.import_folder).isVisible = !isQPlus() findItem(R.id.lock_note).isVisible = mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && !mCurrentNote.isLocked()) findItem(R.id.unlock_note).isVisible = mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && mCurrentNote.isLocked()) + findItem(R.id.make_read_only).isVisible = + mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && !mCurrentNote.isReadOnly) + findItem(R.id.allow_editing).isVisible = + mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && mCurrentNote.isReadOnly) findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations) findItem(R.id.delete_note).isVisible = !isDefaultEmptyNote || mNotes.size > 1 saveNoteButton = findItem(R.id.save_note) saveNoteButton!!.isVisible = - !config.autosaveNotes && showSaveButton && (::mCurrentNote.isInitialized && mCurrentNote.type == NoteType.TYPE_TEXT) + !config.autosaveNotes && showSaveButton && (::mCurrentNote.isInitialized && mCurrentNote.type == NoteType.TYPE_TEXT && !mCurrentNote.isReadOnly) } binding.pagerTabStrip.beVisibleIf(multipleNotesExist) @@ -307,6 +311,7 @@ class MainActivity : SimpleActivity() { R.id.cab_create_shortcut -> createShortcut() R.id.lock_note -> lockNote() R.id.unlock_note -> unlockNote() + R.id.make_read_only, R.id.allow_editing -> fragment?.handleUnlocking { toggleReadOnly() } R.id.open_file -> tryOpenFile() R.id.import_folder -> openFolder() R.id.export_as_file -> fragment?.handleUnlocking { tryExportAsFile() } @@ -582,13 +587,15 @@ class MainActivity : SimpleActivity() { onPageChangeListener { mCurrentNote = mNotes[it] config.currentNoteId = mCurrentNote.id!! + applyReadOnlyStateToCurrentNote() refreshMenuItems() } } - if (!config.showKeyboard || mCurrentNote.type == NoteType.TYPE_CHECKLIST) { + if (!config.showKeyboard || mCurrentNote.type == NoteType.TYPE_CHECKLIST || mCurrentNote.isReadOnly) { hideKeyboard() } + applyReadOnlyStateToCurrentNote() refreshMenuItems() } } @@ -744,6 +751,7 @@ class MainActivity : SimpleActivity() { val index = getNoteIndexWithId(id) binding.viewPager.currentItem = index mCurrentNote = mNotes[index] + applyReadOnlyStateToCurrentNote() } } @@ -1334,6 +1342,10 @@ class MainActivity : SimpleActivity() { } private fun addTextToCurrentNote(text: String) { + if (::mCurrentNote.isInitialized && mCurrentNote.isReadOnly) { + toast(R.string.note_is_read_only) + return + } getPagerAdapter().appendText(binding.viewPager.currentItem, text) } @@ -1599,4 +1611,29 @@ class MainActivity : SimpleActivity() { updateWidgets() } } + + private fun toggleReadOnly() { + if (!::mCurrentNote.isInitialized) { + return + } + + mCurrentNote.isReadOnly = !mCurrentNote.isReadOnly + if (mCurrentNote.isReadOnly) { + showUndoButton = false + showRedoButton = false + showSaveButton = false + } + NotesHelper(this).insertOrUpdateNote(mCurrentNote) { + mNotes.firstOrNull { it.id == mCurrentNote.id }?.isReadOnly = mCurrentNote.isReadOnly + applyReadOnlyStateToCurrentNote() + refreshMenuItems() + } + } + + private fun applyReadOnlyStateToCurrentNote() { + if (!::mCurrentNote.isInitialized) { + return + } + getCurrentFragment()?.updateReadOnlyState(mCurrentNote.isReadOnly) + } } diff --git a/app/src/main/kotlin/org/fossify/notes/adapters/TasksAdapter.kt b/app/src/main/kotlin/org/fossify/notes/adapters/TasksAdapter.kt index 466fbe179..ac948c2ae 100644 --- a/app/src/main/kotlin/org/fossify/notes/adapters/TasksAdapter.kt +++ b/app/src/main/kotlin/org/fossify/notes/adapters/TasksAdapter.kt @@ -45,6 +45,7 @@ class TasksAdapter( private var touchHelper: ItemTouchHelper? = null private var startReorderDragListener: StartReorderDragListener + private var readOnly = false init { setupDragListener(true) @@ -63,7 +64,7 @@ class TasksAdapter( override fun getActionMenuId() = R.menu.cab_checklist override fun actionItemPressed(id: Int) { - if (selectedKeys.isEmpty()) { + if (readOnly || selectedKeys.isEmpty()) { return } @@ -123,7 +124,7 @@ class TasksAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = getItem(position) - holder.bindView(item, allowSingleClick = true, allowLongClick = true) { itemView, _ -> + holder.bindView(item, allowSingleClick = true, allowLongClick = !readOnly) { itemView, _ -> when (item) { is Task -> setupView(itemView, item, holder) is CompletedTasks -> setupCompletedTasks(itemView, item) @@ -174,9 +175,11 @@ class TasksAdapter( } checklistCheckbox.isChecked = task.isDone + checklistCheckbox.isClickable = !readOnly + checklistCheckbox.isFocusable = !readOnly checklistHolder.isSelected = isSelected - val canMoveTask = !task.isDone || !activity.config.moveDoneChecklistItems + val canMoveTask = !readOnly && (!task.isDone || !activity.config.moveDoneChecklistItems) checklistDragHandle.beVisibleIf(beVisible = canMoveTask && selectedKeys.isNotEmpty()) checklistDragHandle.applyColorFilter(textColor) checklistDragHandle.setOnTouchListener { _, event -> @@ -207,7 +210,18 @@ class TasksAdapter( } } + fun setReadOnly(enabled: Boolean) { + readOnly = enabled + setupDragListener(!enabled) + if (enabled) { + finishActMode() + } + } + override fun onRowMoved(fromPosition: Int, toPosition: Int) { + if (readOnly) { + return + } listener?.moveTask(fromPosition, toPosition) } diff --git a/app/src/main/kotlin/org/fossify/notes/databases/NotesDatabase.kt b/app/src/main/kotlin/org/fossify/notes/databases/NotesDatabase.kt index 9e7aa1a65..4f85125ff 100644 --- a/app/src/main/kotlin/org/fossify/notes/databases/NotesDatabase.kt +++ b/app/src/main/kotlin/org/fossify/notes/databases/NotesDatabase.kt @@ -16,7 +16,7 @@ import org.fossify.notes.models.NoteType import org.fossify.notes.models.Widget import java.util.concurrent.Executors -@Database(entities = [Note::class, Widget::class], version = 4) +@Database(entities = [Note::class, Widget::class], version = 5) abstract class NotesDatabase : RoomDatabase() { abstract fun NotesDao(): NotesDao @@ -42,6 +42,7 @@ abstract class NotesDatabase : RoomDatabase() { .addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_2_3) .addMigrations(MIGRATION_3_4) + .addMigrations(MIGRATION_4_5) .build() db!!.openHelper.setWriteAheadLoggingEnabled(true) } @@ -85,5 +86,11 @@ abstract class NotesDatabase : RoomDatabase() { database.execSQL("ALTER TABLE widgets ADD COLUMN widget_show_title INTEGER NOT NULL DEFAULT 0") } } + + private val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE notes ADD COLUMN is_read_only INTEGER NOT NULL DEFAULT 0") + } + } } } diff --git a/app/src/main/kotlin/org/fossify/notes/extensions/EditText.kt b/app/src/main/kotlin/org/fossify/notes/extensions/EditText.kt index ff7e3bd44..29c7757dd 100644 --- a/app/src/main/kotlin/org/fossify/notes/extensions/EditText.kt +++ b/app/src/main/kotlin/org/fossify/notes/extensions/EditText.kt @@ -5,16 +5,7 @@ import android.text.Spanned import org.fossify.commons.views.MyEditText fun MyEditText.enforcePlainText() { - val stripSpans = InputFilter { source, start, end, _, _, _ -> - if (source !is Spanned) return@InputFilter null - val hasRealStyle = source.getSpans(start, end, Any::class.java) - .any { span -> - (source.getSpanFlags(span) and Spanned.SPAN_COMPOSING) == 0 - } - - if (hasRealStyle) source.subSequence(start, end).toString() else null - } - filters = (filters ?: emptyArray()) + stripSpans + applyReadOnlyFilters(readOnly = false) } fun MyEditText.safeSetSelection(position: Int) { @@ -22,3 +13,23 @@ fun MyEditText.safeSetSelection(position: Int) { setSelection(position.coerceIn(0, length)) } +fun MyEditText.applyReadOnlyFilters(readOnly: Boolean) { + val next = mutableListOf() + if (readOnly) { + next.add { _, _, _, dest, dstart, dend -> + dest.subSequence(dstart, dend) + } + } + next.add(plainTextFilter) + filters = next.toTypedArray() +} + +private val plainTextFilter = InputFilter { source, start, end, _, _, _ -> + if (source !is Spanned) return@InputFilter null + val hasRealStyle = source.getSpans(start, end, Any::class.java) + .any { span -> + (source.getSpanFlags(span) and Spanned.SPAN_COMPOSING) == 0 + } + if (hasRealStyle) source.subSequence(start, end).toString() else null +} + diff --git a/app/src/main/kotlin/org/fossify/notes/fragments/NoteFragment.kt b/app/src/main/kotlin/org/fossify/notes/fragments/NoteFragment.kt index 3eb0835e8..2e52846e6 100644 --- a/app/src/main/kotlin/org/fossify/notes/fragments/NoteFragment.kt +++ b/app/src/main/kotlin/org/fossify/notes/fragments/NoteFragment.kt @@ -77,6 +77,8 @@ abstract class NoteFragment : Fragment() { abstract fun checkLockState() + open fun updateReadOnlyState(isReadOnly: Boolean) {} + interface CommonNoteBinding { val root: View val noteLockedLayout: View diff --git a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt index f444fd2ba..a986440f1 100644 --- a/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt +++ b/app/src/main/kotlin/org/fossify/notes/fragments/TasksFragment.kt @@ -111,6 +111,9 @@ class TasksFragment : NoteFragment(), TasksActionListener { ) setOnClickListener { + if (note?.isReadOnly == true) { + return@setOnClickListener + } showNewItemDialog() (binding.checklistList.adapter as? TasksAdapter)?.finishActMode() } @@ -121,6 +124,9 @@ class TasksFragment : NoteFragment(), TasksActionListener { setTextColor(adjustedPrimaryColor) underlineText() setOnClickListener { + if (note?.isReadOnly == true) { + return@setOnClickListener + } showNewItemDialog() } } @@ -132,12 +138,21 @@ class TasksFragment : NoteFragment(), TasksActionListener { } binding.apply { + val canEdit = (!note!!.isLocked() || shouldShowLockedContent) && !note!!.isReadOnly checklistContentHolder.beVisibleIf(!note!!.isLocked() || shouldShowLockedContent) - checklistFab.beVisibleIf(!note!!.isLocked() || shouldShowLockedContent) + checklistFab.beVisibleIf(canEdit) setupLockedViews(this.toCommonBinding(), note!!) } } + override fun updateReadOnlyState(isReadOnly: Boolean) { + note?.isReadOnly = isReadOnly + if (::binding.isInitialized) { + checkLockState() + setupAdapter() + } + } + private fun showNewItemDialog() { NewChecklistItemDialog(activity as SimpleActivity, noteId) { titles -> var currentMaxId = tasks.maxByOrNull { item -> item.id }?.id ?: 0 @@ -206,12 +221,18 @@ class TasksFragment : NoteFragment(), TasksActionListener { tasks.sort() } - getTasksAdapter().submitList(prepareTaskItems()) + getTasksAdapter().apply { + setReadOnly(note?.isReadOnly == true) + submitList(prepareTaskItems()) + } } private fun itemClicked(item: Any) { when (item) { is Task -> { + if (note?.isReadOnly == true) { + return + } val index = tasks.indexOf(item) if (index != -1) { tasks[index] = item.copy(isDone = !item.isDone) @@ -231,6 +252,10 @@ class TasksFragment : NoteFragment(), TasksActionListener { return } + if (note!!.isReadOnly) { + return + } + if (note!!.path.isNotEmpty() && !note!!.path.startsWith("content://") && !File(note!!.path).exists()) { return } @@ -251,12 +276,18 @@ class TasksFragment : NoteFragment(), TasksActionListener { } fun removeCheckedItems() { + if (note?.isReadOnly == true) { + return + } tasks = tasks.filter { !it.isDone }.toMutableList() saveNote() setupAdapter() } fun uncheckAllItems() { + if (note?.isReadOnly == true) { + return + } tasks = tasks.map { it.copy(isDone = false) }.toMutableList() saveAndReload() } @@ -264,7 +295,7 @@ class TasksFragment : NoteFragment(), TasksActionListener { private fun updateUIVisibility() { binding.apply { fragmentPlaceholder.beVisibleIf(tasks.isEmpty()) - fragmentPlaceholder2.beVisibleIf(tasks.isEmpty()) + fragmentPlaceholder2.beVisibleIf(tasks.isEmpty() && note?.isReadOnly != true) checklistList.beVisibleIf(tasks.isNotEmpty()) } } @@ -272,6 +303,9 @@ class TasksFragment : NoteFragment(), TasksActionListener { fun getTasks() = Gson().toJson(tasks) override fun editTask(task: Task, callback: () -> Unit) { + if (note?.isReadOnly == true) { + return + } EditTaskDialog(activity as SimpleActivity, task.title) { title -> val editedTask = task.copy(title = title) val index = tasks.indexOf(task) @@ -282,11 +316,17 @@ class TasksFragment : NoteFragment(), TasksActionListener { } override fun deleteTasks(tasksToDelete: List) { + if (note?.isReadOnly == true) { + return + } tasks.removeAll(tasksToDelete) saveAndReload() } override fun moveTask(fromPosition: Int, toPosition: Int) { + if (note?.isReadOnly == true) { + return + } switchToCustomSorting() val sortableIndices = mutableListOf() @@ -326,6 +366,9 @@ class TasksFragment : NoteFragment(), TasksActionListener { } private fun moveTasks(taskIds: List, targetPosition: Int) { + if (note?.isReadOnly == true) { + return + } switchToCustomSorting() taskIds.forEach { id -> val position = tasks.indexOfFirst { it.id == id } diff --git a/app/src/main/kotlin/org/fossify/notes/fragments/TextFragment.kt b/app/src/main/kotlin/org/fossify/notes/fragments/TextFragment.kt index bdcd7e178..b31c1e558 100644 --- a/app/src/main/kotlin/org/fossify/notes/fragments/TextFragment.kt +++ b/app/src/main/kotlin/org/fossify/notes/fragments/TextFragment.kt @@ -5,6 +5,7 @@ import android.content.Context import android.graphics.Typeface import android.os.Bundle import android.text.Editable +import android.text.InputType import android.text.Selection import android.text.TextWatcher import android.text.style.UnderlineSpan @@ -28,6 +29,7 @@ import org.fossify.notes.activities.MainActivity import org.fossify.notes.databinding.FragmentTextBinding import org.fossify.notes.databinding.NoteViewHorizScrollableBinding import org.fossify.notes.databinding.NoteViewStaticBinding +import org.fossify.notes.extensions.applyReadOnlyFilters import org.fossify.notes.extensions.config import org.fossify.notes.extensions.enforcePlainText import org.fossify.notes.extensions.getPercentageFontSize @@ -120,7 +122,7 @@ class TextFragment : NoteFragment() { if (menuVisible && noteId != 0L) { val currentText = getCurrentNoteViewText() if (currentText != null) { - (activity as MainActivity).currentNoteTextChanged(currentText, isUndoAvailable(), isRedoAvailable()) + (activity as MainActivity).currentNoteTextChanged(currentText, canUndo(), canRedo()) } } } @@ -169,7 +171,7 @@ class TextFragment : NoteFragment() { setSelection(if (config.placeCursorToEnd) text!!.length else 0) } - if (config.showKeyboard && isMenuVisible && (!note!!.isLocked() || shouldShowLockedContent)) { + if (config.showKeyboard && isMenuVisible && !note!!.isReadOnly && (!note!!.isLocked() || shouldShowLockedContent)) { onGlobalLayout { if (activity?.isDestroyed == false) { requestFocus() @@ -179,6 +181,7 @@ class TextFragment : NoteFragment() { } } maybeRequestIncognito() + updateReadOnlyState(note!!.isReadOnly) } noteEditText.setOnTouchListener { v, event -> @@ -203,6 +206,38 @@ class TextFragment : NoteFragment() { setTextWatcher() } + override fun updateReadOnlyState(isReadOnly: Boolean) { + note?.isReadOnly = isReadOnly + if (!::noteEditText.isInitialized) { + return + } + + removeTextWatcher() + val selection = noteEditText.selectionEnd + noteEditText.apply { + val baseInputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE + inputType = if (isReadOnly) { + baseInputType or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + } else { + baseInputType + } + showSoftInputOnFocus = !isReadOnly + isCursorVisible = true + isLongClickable = true + setTextIsSelectable(true) + applyReadOnlyFilters(isReadOnly) + maybeRequestIncognito() + if (isReadOnly) { + activity?.hideKeyboard(this) + } + val length = text?.length ?: 0 + if (length > 0) { + setSelection(selection.coerceIn(0, length)) + } + } + setTextWatcher() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupKeyboardListener() @@ -255,6 +290,11 @@ class TextFragment : NoteFragment() { return } + if (note!!.isReadOnly) { + note?.let { callback?.invoke(it) } + return + } + val newText = getCurrentNoteViewText() val oldText = note!!.getNoteStoredValue(requireContext()) if (newText != null && (newText != oldText || force)) { @@ -278,6 +318,9 @@ class TextFragment : NoteFragment() { } fun undo() { + if (note?.isReadOnly == true) { + return + } val edit = textHistory.getPrevious() ?: return val text = noteEditText.editableText @@ -308,6 +351,9 @@ class TextFragment : NoteFragment() { } fun redo() { + if (note?.isReadOnly == true) { + return + } val edit = textHistory.getNext() ?: return val text = noteEditText.editableText @@ -335,6 +381,10 @@ class TextFragment : NoteFragment() { fun isRedoAvailable() = textHistory.position < textHistory.history.size + private fun canUndo() = note?.isReadOnly != true && isUndoAvailable() + + private fun canRedo() = note?.isReadOnly != true && isRedoAvailable() + private var textWatcher: TextWatcher = object : TextWatcher { private var beforeChange: CharSequence? = null private var afterChange: CharSequence? = null @@ -355,7 +405,7 @@ class TextFragment : NoteFragment() { override fun afterTextChanged(editable: Editable) { val text = editable.toString() setWordCounter(text) - (activity as MainActivity).currentNoteTextChanged(text, isUndoAvailable(), isRedoAvailable()) + (activity as MainActivity).currentNoteTextChanged(text, canUndo(), canRedo()) } } diff --git a/app/src/main/kotlin/org/fossify/notes/models/Note.kt b/app/src/main/kotlin/org/fossify/notes/models/Note.kt index 0aecc47b2..05c3deba0 100644 --- a/app/src/main/kotlin/org/fossify/notes/models/Note.kt +++ b/app/src/main/kotlin/org/fossify/notes/models/Note.kt @@ -25,7 +25,8 @@ data class Note( @ColumnInfo(name = "type") var type: NoteType, @ColumnInfo(name = "path") var path: String, @ColumnInfo(name = "protection_type") var protectionType: Int, - @ColumnInfo(name = "protection_hash") var protectionHash: String + @ColumnInfo(name = "protection_hash") var protectionHash: String, + @ColumnInfo(name = "is_read_only", defaultValue = "0") var isReadOnly: Boolean = false ) { fun getNoteStoredValue(context: Context): String? { diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index 3e0266844..58689d6dd 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -66,6 +66,14 @@ android:id="@+id/unlock_note" android:title="@string/unlock_note" app:showAsAction="never" /> + + New checklist The app cannot load files over the internet To backup notes automatically, please grant the app permission to schedule exact alarms. + Make read-only + Allow editing + This note is read-only Open file