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 ef4a8e019..6b34db297 100755 --- a/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java +++ b/app/src/main/java/com/git/amarradi/leafpad/NoteEditActivity.java @@ -31,6 +31,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; @@ -38,15 +41,19 @@ public class NoteEditActivity extends AppCompatActivity { private EditText titleEdit; private EditText bodyEdit; + private TextView previewBody; + private Note note; private NoteViewModel noteViewModel; private MaterialToolbar toolbar; private Resources res; private boolean shouldPersistOnPause = true; private boolean isNoteDeleted = false; private NestedScrollView bodyScroll; + private boolean isNewNote = false; private boolean fromSearch = false; private boolean isUIConfigured = false; + private MenuItem saveMenuItem; @SuppressLint("MissingInflatedId") @@ -88,7 +95,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); @@ -170,35 +177,50 @@ 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); // Initialize 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) { - bodyEdit.post(() -> scrollToCursor()); + 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.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - bodyEdit.postDelayed(this::scrollToCursor, 250); + @Override + public void afterTextChanged(Editable s) { + 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); + } + }); + } } - }); - - bodyEdit.setOnClickListener(v -> { - bodyEdit.postDelayed(this::scrollToCursor, 250); - }); + }; - titleLayout.setHintEnabled(false); - bodyLayout.setHintEnabled(false); + titleEdit.addTextChangedListener(textWatcher); + bodyEdit.addTextChangedListener(textWatcher); } private void scrollToCursor() { @@ -216,15 +238,50 @@ private boolean isNewEntry(Note note) { note.getBody() == null || note.getBody().isEmpty()); } + 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); - saveMenuItem = menu.findItem(R.id.action_save); - if (saveMenuItem != null) { - saveMenuItem.setEnabled(false); // Initial disabled + + // 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" } - Note current = noteViewModel.getSelectedNote().getValue(); - if (current != null) { + + if (note != null) { MenuItem recipeItem = menu.findItem(R.id.action_recipe); boolean isRecipe = current.getCategory() != null && current.getCategory().equals(res.getStringArray(R.array.category)[0]); @@ -242,6 +299,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; + } + switch (id) { case R.id.action_recipe: { Note current = noteViewModel.getSelectedNote().getValue(); 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(); + } +} 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 2aafe9952..9f335c95f 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<>(); @@ -44,10 +49,30 @@ public LiveData getReleaseNote() { } 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()); + // 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 { + return Html.fromHtml(html); + } + }); private final MediatorLiveData isNoteModified = new MediatorLiveData<>(); + 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 @@ -285,6 +310,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; 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 diff --git a/app/src/main/res/layout/activity_note_edit.xml b/app/src/main/res/layout/activity_note_edit.xml index 3d7976d20..33f7b8f62 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_margin_start" android:paddingEnd="@dimen/switch_layout_margin_end"> - + + + + diff --git a/app/src/main/res/menu/menu_note_edit.xml b/app/src/main/res/menu/menu_note_edit.xml index 8b12ae4a4..4f238badd 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/ic_share" android:title="@string/action_share_note" app:showAsAction="ifRoom" /> + + + add new Note note was send to leafpad edited at + preview + new Note Enjoying leafpad? How about a review in Google\'s PlayStore?