From f5fe88db33611a258f83ad300d18bac84218e9a8 Mon Sep 17 00:00:00 2001 From: Gerardo Ramos Date: Thu, 17 Jul 2025 09:08:23 -0600 Subject: [PATCH 1/7] Created MarkdownParser --- .../leafpad/helper/MarkdownParser.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 app/src/main/java/com/git/amarradi/leafpad/helper/MarkdownParser.java diff --git a/app/src/main/java/com/git/amarradi/leafpad/helper/MarkdownParser.java b/app/src/main/java/com/git/amarradi/leafpad/helper/MarkdownParser.java new file mode 100644 index 000000000..293744706 --- /dev/null +++ b/app/src/main/java/com/git/amarradi/leafpad/helper/MarkdownParser.java @@ -0,0 +1,33 @@ +package com.git.amarradi.leafpad.helper; + +public class MarkdownParser { + + public static String parse(String rawText) { + StringBuilder result = new StringBuilder(); + String[] lines = rawText.split("\n"); + + for (String line : lines) { + String parsedLine = line; + + // Heading: "# Heading" + if (parsedLine.startsWith("# ")) { + parsedLine = "

" + parsedLine.substring(2).trim() + "

"; + } + + // Bullet point: "* bullet" + else if (parsedLine.startsWith("* ")) { + parsedLine = context.getString(R.string.bullet_point_symbol) + parsedLine.substring(2).trim(); + } + + // Underline: "__underlined__" + parsedLine = parsedLine.replaceAll("__(.*?)__", "$1"); + + // Link: [text](url) + parsedLine = parsedLine.replaceAll("\\[(.+?)\\]\\((http.*?)\\)", "$1"); + + result.append(parsedLine).append("
"); + } + + return result.toString(); + } +} From 0d72275eab5046db4edea02090de4997b53bc29a Mon Sep 17 00:00:00 2001 From: Gerardo Ramos Date: Thu, 17 Jul 2025 09:09:12 -0600 Subject: [PATCH 2/7] Added implementation for MarkdownParser to style text --- .../amarradi/leafpad/NoteEditActivity.java | 122 ++++++++++++------ .../leafpad/viewmodel/NoteViewModel.java | 41 +++++- 2 files changed, 119 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java b/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java index ec4629829..bb6b1f79d 100755 --- a/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java +++ b/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java @@ -29,6 +29,9 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.textfield.TextInputLayout; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; + import java.util.List; import java.util.Objects; @@ -36,6 +39,7 @@ public class NoteEditActivity extends AppCompatActivity { private EditText titleEdit; private EditText bodyEdit; + private TextView previewBody; private Note note; private NoteViewModel noteViewModel; private MaterialToolbar toolbar; @@ -44,14 +48,11 @@ public class NoteEditActivity extends AppCompatActivity { private boolean isNoteDeleted = false; // <--- Flag setzen! private NestedScrollView bodyScroll; - - private boolean isNewNote = false; private boolean fromSearch = false; private boolean isUIConfigured = false; - @SuppressLint("MissingInflatedId") @Override protected void onCreate(Bundle savedInstanceState) { @@ -85,7 +86,7 @@ protected void onCreate(Bundle savedInstanceState) { } handleIntent(getIntent()); fromSearch = getIntent().getBooleanExtra("fromSearch", false); - observeNote(); + observeViewModel(); View rootEdit = findViewById(R.id.all); View toolbar = findViewById(R.id.toolbar); @@ -180,59 +181,49 @@ private void configureUIFromNote(Note note) { } private void initViews() { - TextInputLayout titleLayout = findViewById(R.id.default_text_input_layout); - TextInputLayout bodyLayout = findViewById(R.id.body_text_input_layout); titleEdit = findViewById(R.id.title_edit); bodyEdit = findViewById(R.id.body_edit); - + previewBody = findViewById(R.id.preview_body); // NUEVO: Inicializamos el TextView bodyScroll = findViewById(R.id.body_scroll); - bodyEdit.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + // clickable links on preview + previewBody.setMovementMethod(LinkMovementMethod.getInstance()); + // one TextWatcher to update ViewModel on real time + TextWatcher textWatcher = new TextWatcher() { @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { } + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override - public void afterTextChanged(Editable s) { - // Stelle sicher, dass Cursor immer sichtbar ist - bodyEdit.post(() -> { - int selection = bodyEdit.getSelectionStart(); - Layout layout = bodyEdit.getLayout(); - if (layout != null && selection > 0) { - int line = layout.getLineForOffset(selection); - int y = layout.getLineBottom(line); - bodyScroll.smoothScrollTo(0, y); + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (noteViewModel.getSelectedNote().getValue() != null) { + if (getCurrentFocus() == titleEdit) { + noteViewModel.updateNoteTitle(s.toString()); + } else if (getCurrentFocus() == bodyEdit) { + noteViewModel.updateNoteBody(s.toString()); } - }); + } } - }); - - bodyEdit.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { - // Stelle sicher, dass Cursor immer sichtbar ist - bodyEdit.post(() -> { - int selection = bodyEdit.getSelectionStart(); - Layout layout = bodyEdit.getLayout(); - if (layout != null && selection > 0) { - int line = layout.getLineForOffset(selection); - int y = layout.getLineBottom(line); - bodyScroll.smoothScrollTo(0, y); - } - }); + if (getCurrentFocus() == bodyEdit) { + // Stelle sicher, dass Cursor immer sichtbar ist + bodyEdit.post(() -> { + int selection = bodyEdit.getSelectionStart(); + Layout layout = bodyEdit.getLayout(); + if (layout != null && selection > 0) { + int line = layout.getLineForOffset(selection); + int y = layout.getLineBottom(line); + bodyScroll.smoothScrollTo(0, y); + } + }); + } } - }); + }; - titleLayout.setHintEnabled(false); - bodyLayout.setHintEnabled(false); + titleEdit.addTextChangedListener(textWatcher); + bodyEdit.addTextChangedListener(textWatcher); } private boolean isNewEntry(Note note) { @@ -251,9 +242,49 @@ private boolean isNewEntry(Note note) { return false; } + private void observeViewModel() { + // Observer for selected note (initial launch) + noteViewModel.getSelectedNote().observe(this, currentNote -> { + if (currentNote != null && !isUIConfigured) { + this.note = currentNote; + titleEdit.setText(currentNote.getTitle()); + bodyEdit.setText(currentNote.getBody()); + isUIConfigured = true; // Prevents reload when rotating screen + invalidateOptionsMenu(); + } + }); + + // observer for state of the preview + noteViewModel.isPreviewActive().observe(this, isActive -> { + bodyEdit.setVisibility(isActive ? View.GONE : View.VISIBLE); + previewBody.setVisibility(isActive ? View.VISIBLE : View.GONE); + + // if preview is active, force refresh of Spanned + if (isActive) { + noteViewModel.getSelectedNote().setValue(noteViewModel.getSelectedNote().getValue()); + } + + invalidateOptionsMenu(); // refresh menu icon + }); + + // Observer for parsed body (updates TextView of the preview) + noteViewModel.parsedBodyAsSpanned.observe(this, spanned -> { + previewBody.setText(spanned); + }); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_note_edit, menu); + + // Updates preview icon depending on state + MenuItem previewItem = menu.findItem(R.id.action_preview); + if (noteViewModel.isPreviewActive().getValue() != null && noteViewModel.isPreviewActive().getValue()) { + previewItem.setIcon(R.drawable.ic_edit); // Changes to icon "edit" + } else { + previewItem.setIcon(R.drawable.ic_preview); // icon "preview" + } + if (note != null) { MenuItem recipeItem = menu.findItem(R.id.action_recipe); boolean isRecipe = note.getCategory() != null && note.getCategory().equals(res.getStringArray(R.array.category)[0]); @@ -285,6 +316,12 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); + + if (id == R.id.action_preview) { + noteViewModel.togglePreview(); + return true; + } + return switch (id) { case R.id.action_recipe -> { @@ -326,7 +363,6 @@ public void updateNoteFromUI() { current.setTitle(titleEdit.getText().toString()); current.setBody(bodyEdit.getText().toString()); - } private void exitNoteEdit() { diff --git a/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java b/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java index 2b1844061..7b7d323fd 100644 --- a/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java +++ b/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java @@ -2,6 +2,9 @@ import android.app.Application; import android.content.Context; +import android.os.Build; +import android.text.Html; +import android.text.Spanned; import android.util.Log; import androidx.annotation.NonNull; @@ -12,6 +15,7 @@ import androidx.lifecycle.Transformations; import com.git.amarradi.leafpad.Leafpad; +import com.git.amarradi.leafpad.helper.MarkdownParser; import com.git.amarradi.leafpad.helper.ReleaseNoteHelper; import com.git.amarradi.leafpad.model.Leaf; import com.git.amarradi.leafpad.model.Note; @@ -21,7 +25,8 @@ import java.util.List; import java.util.Objects; -public class NoteViewModel extends AndroidViewModel { + +public class NoteViewModel extends AndroidViewModel { private final MutableLiveData> notesLiveData = new MutableLiveData<>(); private static final MutableLiveData selectedNote = new MutableLiveData<>(); @@ -45,8 +50,29 @@ public LiveData getReleaseNote() { // In NoteViewModel.java private final MediatorLiveData> combinedNotes = new MediatorLiveData<>(); public LiveData> getCombinedNotes() { return combinedNotes; } + private final MutableLiveData isPreviewActive = new MutableLiveData<>(false); + + public final LiveData parsedBodyAsSpanned = Transformations.map(selectedNote, note -> { + if (note == null || note.getBody() == null) { + return Html.fromHtml("", Html.FROM_HTML_MODE_LEGACY); + } + String html = MarkdownParser.parse(note.getBody()); + // Usamos Html.fromHtml para convertir nuestro HTML a un objeto Spanned que TextView puede renderizar. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); + } else { + // Versión deprecada para APIs antiguas + return Html.fromHtml(html); + } + }); + public LiveData isPreviewActive() { + return isPreviewActive; + } + public void togglePreview() { + isPreviewActive.setValue(Boolean.FALSE.equals(isPreviewActive.getValue())); + } public void checkAndLoadReleaseNote(Context context) { int savedVersion = Leafpad.getCurrentLeafpadVersionCode(context); // default = 0 @@ -273,6 +299,19 @@ public void setNote(Note note) { selectedNote.setValue(new Note(note)); } } + public void updateNoteBody(String newBody) { + Note currentNote = selectedNote.getValue(); + if (currentNote != null && !Objects.equals(currentNote.getBody(), newBody)) { + currentNote.setBody(newBody); + } + } + + public void updateNoteTitle(String newTitle) { + Note currentNote = selectedNote.getValue(); + if (currentNote != null && !Objects.equals(currentNote.getTitle(), newTitle)) { + currentNote.setTitle(newTitle); + } + } public void loadNotes() { Boolean showHidden = showHiddenLiveData.getValue(); if (showHidden == null) showHidden = false; From e9c3ffe2300eec1cb1b2cdd0943a43aeb3dbec2b Mon Sep 17 00:00:00 2001 From: Gerardo Ramos Date: Thu, 17 Jul 2025 09:10:04 -0600 Subject: [PATCH 3/7] created icons for preview and edit button --- app/src/main/res/drawable/ic_edit.xml | 10 ++++++++++ app/src/main/res/drawable/ic_preview.xml | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/drawable/ic_preview.xml diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..a4bfc004b --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_preview.xml b/app/src/main/res/drawable/ic_preview.xml new file mode 100644 index 000000000..186f58c47 --- /dev/null +++ b/app/src/main/res/drawable/ic_preview.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file From f9f911e3ad6db8fbb5af64491409e99012019de1 Mon Sep 17 00:00:00 2001 From: Gerardo Ramos Date: Thu, 17 Jul 2025 09:10:37 -0600 Subject: [PATCH 4/7] Added strings to use --- app/src/main/res/values/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e1f0ab21..c107b84c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,8 @@ add new Note note was send to leafpad edited at + preview + new Note Enjoying leafpad? How about a review in Google\'s PlayStore? From e5e1f25a43d348c201fab9396c27b477be72f9a8 Mon Sep 17 00:00:00 2001 From: Gerardo Ramos Date: Thu, 17 Jul 2025 09:11:36 -0600 Subject: [PATCH 5/7] Updated note edits for preview --- .../main/res/layout/activity_note_edit.xml | 37 ++++++++++++++----- app/src/main/res/menu/menu_note_edit.xml | 7 ++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/layout/activity_note_edit.xml b/app/src/main/res/layout/activity_note_edit.xml index 991c17cae..18d88c5a8 100755 --- a/app/src/main/res/layout/activity_note_edit.xml +++ b/app/src/main/res/layout/activity_note_edit.xml @@ -73,16 +73,33 @@ android:paddingStart="@dimen/switch_layout_marginStart" android:paddingEnd="@dimen/switch_layout_marginEnd"> - + + + + diff --git a/app/src/main/res/menu/menu_note_edit.xml b/app/src/main/res/menu/menu_note_edit.xml index 3988bcc2a..3b48c4a93 100755 --- a/app/src/main/res/menu/menu_note_edit.xml +++ b/app/src/main/res/menu/menu_note_edit.xml @@ -20,6 +20,13 @@ android:icon="@drawable/share" android:title="@string/action_share_note" app:showAsAction="ifRoom" /> + + + Date: Thu, 17 Jul 2025 09:26:49 -0600 Subject: [PATCH 6/7] updated comments --- .../com/git/amarradi/leafpad/viewmodel/NoteViewModel.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java b/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java index 7b7d323fd..2ae048bb1 100644 --- a/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java +++ b/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java @@ -47,7 +47,6 @@ public class NoteViewModel extends AndroidViewModel { public LiveData getReleaseNote() { return releaseNoteLiveData; } - // In NoteViewModel.java private final MediatorLiveData> combinedNotes = new MediatorLiveData<>(); public LiveData> getCombinedNotes() { return combinedNotes; } private final MutableLiveData isPreviewActive = new MutableLiveData<>(false); @@ -57,11 +56,10 @@ public LiveData getReleaseNote() { return Html.fromHtml("", Html.FROM_HTML_MODE_LEGACY); } String html = MarkdownParser.parse(note.getBody()); - // Usamos Html.fromHtml para convertir nuestro HTML a un objeto Spanned que TextView puede renderizar. + // Use Html.fromHtml to convert our HTML to an Spanned objecto that TextView can render if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); } else { - // Versión deprecada para APIs antiguas return Html.fromHtml(html); } }); From 2fadad58e28ff1924471350a3203e82c723006ae Mon Sep 17 00:00:00 2001 From: Gerardo Ramos Date: Thu, 17 Jul 2025 09:29:34 -0600 Subject: [PATCH 7/7] Fixed typo --- .../main/java/com/git/amarradi/leafpad/NoteEditActivity.java | 2 +- .../java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java b/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java index bb6b1f79d..5d007e170 100755 --- a/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java +++ b/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java @@ -183,7 +183,7 @@ private void configureUIFromNote(Note note) { private void initViews() { titleEdit = findViewById(R.id.title_edit); bodyEdit = findViewById(R.id.body_edit); - previewBody = findViewById(R.id.preview_body); // NUEVO: Inicializamos el TextView + previewBody = findViewById(R.id.preview_body); // Initialize TextView bodyScroll = findViewById(R.id.body_scroll); // clickable links on preview diff --git a/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java b/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java index 2ae048bb1..8a93d356a 100644 --- a/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java +++ b/app/src/main/java/com/git/amarradi/leafpad/viewmodel/NoteViewModel.java @@ -56,7 +56,7 @@ public LiveData getReleaseNote() { return Html.fromHtml("", Html.FROM_HTML_MODE_LEGACY); } String html = MarkdownParser.parse(note.getBody()); - // Use Html.fromHtml to convert our HTML to an Spanned objecto that TextView can render + // Use Html.fromHtml to convert our HTML to an Spanned object that TextView can render if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); } else {