diff --git a/app/build.gradle b/app/build.gradle index c87df9d6..5fbbaa46 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,6 +119,7 @@ dependencies { implementation "androidx.media3:media3-ui:1.1.0" annotationProcessor 'androidx.room:room-compiler:2.6.1' implementation 'net.zetetic:android-database-sqlcipher:4.5.4' + implementation 'io.noties.markwon:core:4.6.2' testImplementation 'junit:junit:4.13.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.4' diff --git a/app/src/main/java/app/notesr/activity/note/DeleteNoteOnClick.java b/app/src/main/java/app/notesr/activity/note/DeleteNoteOnClick.java new file mode 100644 index 00000000..a6585966 --- /dev/null +++ b/app/src/main/java/app/notesr/activity/note/DeleteNoteOnClick.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 zHd4 + * SPDX-License-Identifier: MIT + */ + +package app.notesr.activity.note; + +import static java.util.concurrent.Executors.newSingleThreadExecutor; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.view.MenuItem; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +import app.notesr.R; +import app.notesr.activity.ActivityBase; +import app.notesr.activity.DialogFactory; +import app.notesr.data.model.Note; +import app.notesr.service.file.FileService; +import app.notesr.service.note.NoteService; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class DeleteNoteOnClick implements MenuItem.OnMenuItemClickListener { + + private final ActivityBase activity; + private final Note note; + private final NoteService noteService; + private final FileService fileService; + private final DialogFactory dialogFactory; + + @Override + public boolean onMenuItemClick(@NonNull MenuItem item) { + DialogInterface.OnClickListener buttonHandler = deleteNoteDialogOnClick(); + dialogFactory.getThemedAlertDialogBuilder(R.layout.dialog_action_cannot_be_undo) + .setTitle(R.string.warning) + .setPositiveButton(R.string.delete, buttonHandler) + .setNegativeButton(R.string.no, buttonHandler) + .create() + .show(); + + return true; + } + + private DialogInterface.OnClickListener deleteNoteDialogOnClick() { + return (dialog, result) -> { + if (result == DialogInterface.BUTTON_POSITIVE) { + Dialog progressDialog = dialogFactory + .getThemedProgressDialog(R.layout.progress_dialog_deleting); + + newSingleThreadExecutor().execute(() -> { + activity.runOnUiThread(progressDialog::show); + + try { + noteService.delete(note.getId(), fileService); + } catch (IOException e) { + throw new RuntimeException(e); + } + + activity.runOnUiThread(() -> { + progressDialog.dismiss(); + activity.startActivity(new Intent(activity.getApplicationContext(), + NotesListActivity.class)); + }); + }); + } + }; + } +} diff --git a/app/src/main/java/app/notesr/activity/note/OpenFilesListOnClick.java b/app/src/main/java/app/notesr/activity/note/OpenFilesListOnClick.java new file mode 100644 index 00000000..8759ce8b --- /dev/null +++ b/app/src/main/java/app/notesr/activity/note/OpenFilesListOnClick.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2026 zHd4 + * SPDX-License-Identifier: MIT + */ + +package app.notesr.activity.note; + +import android.content.Intent; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.NonNull; + +import app.notesr.activity.ActivityBase; +import app.notesr.activity.file.FilesListActivity; +import app.notesr.data.model.Note; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class OpenFilesListOnClick implements MenuItem.OnMenuItemClickListener, View.OnClickListener { + + private final ActivityBase activity; + private final Note note; + + @Override + public boolean onMenuItemClick(@NonNull MenuItem item) { + onClickAction(); + return true; + } + + @Override + public void onClick(View v) { + onClickAction(); + } + + private void onClickAction() { + Intent intent = new Intent(activity.getApplicationContext(), FilesListActivity.class); + + intent.putExtra(FilesListActivity.EXTRA_NOTE_ID, note.getId()); + activity.startActivity(intent); + } +} diff --git a/app/src/main/java/app/notesr/activity/note/OpenNoteActivity.java b/app/src/main/java/app/notesr/activity/note/OpenNoteActivity.java index e01eec08..523c0ba1 100644 --- a/app/src/main/java/app/notesr/activity/note/OpenNoteActivity.java +++ b/app/src/main/java/app/notesr/activity/note/OpenNoteActivity.java @@ -5,18 +5,20 @@ package app.notesr.activity.note; -import android.app.Dialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.text.Editable; +import android.text.method.LinkMovementMethod; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; +import android.widget.PopupMenu; +import android.widget.ScrollView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.core.widget.TextViewKt; @@ -25,7 +27,6 @@ import app.notesr.activity.DialogFactory; import app.notesr.data.AppDatabase; import app.notesr.data.DatabaseProvider; -import app.notesr.activity.file.FilesListActivity; import app.notesr.service.file.FileService; import app.notesr.data.model.Note; import app.notesr.service.note.NoteService; @@ -34,15 +35,11 @@ import app.notesr.core.security.crypto.CryptoManagerProvider; import app.notesr.core.security.dto.CryptoSecrets; import app.notesr.core.util.FilesUtils; +import io.noties.markwon.Markwon; import kotlin.Unit; import kotlin.jvm.functions.Function1; -import java.io.IOException; -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; -import java.util.function.Consumer; import static androidx.core.view.inputmethod.EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING; import static java.util.concurrent.Executors.newSingleThreadExecutor; @@ -52,19 +49,32 @@ public final class OpenNoteActivity extends ActivityBase { public static final String EXTRA_NOTE_ID = "noteId"; public static final String EXTRA_NOTE_MODIFIED = "modified"; private static final long MAX_COUNT_IN_BADGE = 9; - private final Map> menuItemsMap = new HashMap<>(); private NoteService noteService; private FileService fileService; private Note note; + private ActionBar actionBar; private Menu activityMenu; private DialogFactory dialogFactory; private boolean isNoteModified; + private EditText nameField; + private EditText textField; + private TextView markdownViewer; + private ScrollView markdownViewerContainer; + private Markwon markwon; + private static final String STATE_OPEN_MODE = "openMode"; + private OpenNoteMode openMode = OpenNoteMode.EDIT; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + openMode = OpenNoteMode.fromCode(savedInstanceState.getInt(STATE_OPEN_MODE)); + } else { + openMode = OpenNoteMode.EDIT; + } + if (isFinishing()) { return; } @@ -81,47 +91,60 @@ protected void onCreate(Bundle savedInstanceState) { noteService = new NoteService(db); fileService = new FileService(context, db, cryptor, new FilesUtils()); dialogFactory = new DialogFactory(this); + markwon = Markwon.create(this); String noteId = getIntent().getStringExtra(EXTRA_NOTE_ID); newSingleThreadExecutor().execute(() -> { note = noteService.get(noteId); + + if (isNewNote()) { + note = new Note(); + } + isNoteModified = getIntent().getBooleanExtra(EXTRA_NOTE_MODIFIED, false); runOnUiThread(() -> { initializeActionBar(); - prepareEditorFields(); + prepareViews(); + + switch (openMode) { + case MARKDOWN_VIEW: + switchToViewMarkdownMode(); + break; + case EDIT: + default: + switchToEditMode(); + break; + } }); }); } private void initializeActionBar() { - ActionBar actionBar = getSupportActionBar(); + actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); - if (note != null) { - actionBar.setTitle(getResources().getString(R.string.edit_note)); - } else { - actionBar.setTitle(getResources().getString(R.string.new_note)); - } + int titleId = isNewNote() ? R.string.new_note : R.string.edit; + actionBar.setTitle(getResources().getString(titleId)); } else { throw new NullPointerException("Action bar is null"); } } - private void prepareEditorFields() { - EditText nameField = findViewById(R.id.noteNameField); - EditText textField = findViewById(R.id.noteTextField); + private void prepareViews() { + nameField = findViewById(R.id.noteNameField); + textField = findViewById(R.id.noteTextField); + markdownViewer = findViewById(R.id.markdownViewer); + markdownViewerContainer = findViewById(R.id.markdownViewerContainer); nameField.setImeOptions(IME_FLAG_NO_PERSONALIZED_LEARNING); textField.setImeOptions(IME_FLAG_NO_PERSONALIZED_LEARNING); - if (note != null) { - nameField.setText(note.getName()); - textField.setText(note.getText()); - } + nameField.setText(note.getName()); + textField.setText(note.getText()); Function1 afterTextChangedAction = editable -> { if (!isNoteModified) { @@ -138,27 +161,35 @@ private void prepareEditorFields() { TextViewKt.doAfterTextChanged(textField, afterTextChangedAction); } + @SuppressWarnings("ConstantValue") // Because note id could be null before first save + private boolean isNewNote() { + return note == null || note.getId() == null; + } + @Override public boolean onCreateOptionsMenu(Menu menu) { - EditText nameField = findViewById(R.id.noteNameField); - EditText textField = findViewById(R.id.noteTextField); - getMenuInflater().inflate(R.menu.menu_open_note, menu); this.activityMenu = menu; + MenuItem changeModeButton = menu.findItem(R.id.changeOpenModeButton); MenuItem saveNoteButton = menu.findItem(R.id.saveNoteButton); MenuItem openFilesListButton = menu.findItem(R.id.openFilesListButton); MenuItem deleteNoteButton = menu.findItem(R.id.deleteNoteButton); - menuItemsMap.put(saveNoteButton.getItemId(), - action -> saveNoteOnClick(nameField, textField)); + changeModeButton.setOnMenuItemClickListener(item -> { + changeOpenModeButtonOnClick(); + return true; + }); - if (note != null) { - menuItemsMap.put(openFilesListButton.getItemId(), - action -> openFilesListOnClick()); + saveNoteButton.setOnMenuItemClickListener(new SaveNoteOnClick(this, note, + noteService, dialogFactory, nameField, textField)); - menuItemsMap.put(deleteNoteButton.getItemId(), - action -> deleteNoteOnClick()); + if (!isNewNote()) { + openFilesListButton.setOnMenuItemClickListener( + new OpenFilesListOnClick(this, note)); + + deleteNoteButton.setOnMenuItemClickListener(new DeleteNoteOnClick(this, note, + noteService, fileService, dialogFactory)); setAttachedFilesCountBadge(openFilesListButton); } else { @@ -190,8 +221,7 @@ private void setAttachedFilesCountBadge(MenuItem openFilesListButton) { badge.setText(badgeText); badge.setVisibility(View.VISIBLE); - - view.setOnClickListener(v -> openFilesListOnClick()); + view.setOnClickListener(new OpenFilesListOnClick(this, note)); } }); }); @@ -212,82 +242,66 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } - Consumer action = menuItemsMap.get(id); - - if (action != null) { - action.accept(null); - } - return super.onOptionsItemSelected(item); } - private void saveNoteOnClick(EditText nameField, EditText textField) { - String name = nameField.getText().toString(); - String text = textField.getText().toString(); + private void changeOpenModeButtonOnClick() { + View anchor = findViewById(R.id.changeOpenModeButton); + PopupMenu popup = new PopupMenu(this, anchor); + popup.inflate(R.menu.menu_open_node_open_mode); - if (!name.isBlank() && !text.isBlank()) { - if (note == null) { - note = new Note(); - } + Menu popupMenu = popup.getMenu(); - note.setName(name); - note.setText(text); - note.setUpdatedAt(LocalDateTime.now()); + if (openMode == OpenNoteMode.EDIT) { + popupMenu.findItem(R.id.editMenuItem).setChecked(true); + } else if (openMode == OpenNoteMode.MARKDOWN_VIEW) { + popupMenu.findItem(R.id.viewMarkdownMenuItem).setChecked(true); + } - Dialog progressDialog = dialogFactory - .getThemedProgressDialog(R.layout.progress_dialog_loading); + popup.setOnMenuItemClickListener(item -> { + int id = item.getItemId(); + + if (id == R.id.editMenuItem) { + item.setChecked(true); + openMode = OpenNoteMode.EDIT; + switchToEditMode(); + return true; + } else if (id == R.id.viewMarkdownMenuItem) { + item.setChecked(true); + openMode = OpenNoteMode.MARKDOWN_VIEW; + switchToViewMarkdownMode(); + return true; + } - newSingleThreadExecutor().execute(() -> { - runOnUiThread(progressDialog::show); - noteService.save(note); + return false; + }); - runOnUiThread(() -> { - progressDialog.dismiss(); - startActivity(new Intent(getApplicationContext(), NotesListActivity.class)); - }); - }); - } + nameField.post(popup::show); } - private void deleteNoteOnClick() { - DialogInterface.OnClickListener buttonHandler = deleteNoteDialogOnClick(); - dialogFactory.getThemedAlertDialogBuilder(R.layout.dialog_action_cannot_be_undo) - .setTitle(R.string.warning) - .setPositiveButton(R.string.delete, buttonHandler) - .setNegativeButton(R.string.no, buttonHandler) - .create() - .show(); + private void switchToEditMode() { + openMode = OpenNoteMode.EDIT; + nameField.setEnabled(true); + textField.setVisibility(View.VISIBLE); + markdownViewerContainer.setVisibility(View.GONE); + actionBar.setTitle(getResources().getString(R.string.edit)); } - private void openFilesListOnClick() { - Intent intent = new Intent(getApplicationContext(), FilesListActivity.class); + private void switchToViewMarkdownMode() { + openMode = OpenNoteMode.MARKDOWN_VIEW; + nameField.setEnabled(false); + textField.setVisibility(View.GONE); - intent.putExtra(FilesListActivity.EXTRA_NOTE_ID, note.getId()); - startActivity(intent); + markwon.setMarkdown(markdownViewer, textField.getText().toString()); + markdownViewer.setMovementMethod(LinkMovementMethod.getInstance()); + markdownViewerContainer.setVisibility(View.VISIBLE); + actionBar.setTitle(getResources().getString(R.string.view_markdown)); } - private DialogInterface.OnClickListener deleteNoteDialogOnClick() { - return (dialog, result) -> { - if (result == DialogInterface.BUTTON_POSITIVE) { - Dialog progressDialog = dialogFactory - .getThemedProgressDialog(R.layout.progress_dialog_deleting); - - newSingleThreadExecutor().execute(() -> { - runOnUiThread(progressDialog::show); - - try { - noteService.delete(note.getId(), fileService); - } catch (IOException e) { - throw new RuntimeException(e); - } - - runOnUiThread(() -> { - progressDialog.dismiss(); - startActivity(new Intent(getApplicationContext(), NotesListActivity.class)); - }); - }); - } - }; + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(STATE_OPEN_MODE, openMode.getModeCode()); } private void disableMenuItem(MenuItem item) { diff --git a/app/src/main/java/app/notesr/activity/note/OpenNoteMode.java b/app/src/main/java/app/notesr/activity/note/OpenNoteMode.java new file mode 100644 index 00000000..285bede2 --- /dev/null +++ b/app/src/main/java/app/notesr/activity/note/OpenNoteMode.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 zHd4 + * SPDX-License-Identifier: MIT + */ + +package app.notesr.activity.note; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum OpenNoteMode { + EDIT(0), + MARKDOWN_VIEW(1); + + private final int modeCode; + + public static OpenNoteMode fromCode(int code) { + for (OpenNoteMode mode : values()) { + if (mode.getModeCode() == code) { + return mode; + } + } + + return null; + } +} diff --git a/app/src/main/java/app/notesr/activity/note/SaveNoteOnClick.java b/app/src/main/java/app/notesr/activity/note/SaveNoteOnClick.java new file mode 100644 index 00000000..1b1c2104 --- /dev/null +++ b/app/src/main/java/app/notesr/activity/note/SaveNoteOnClick.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 zHd4 + * SPDX-License-Identifier: MIT + */ + +package app.notesr.activity.note; + +import static java.util.concurrent.Executors.newSingleThreadExecutor; + +import android.app.Dialog; +import android.content.Intent; +import android.view.MenuItem; +import android.widget.EditText; + +import androidx.annotation.NonNull; + +import java.time.LocalDateTime; + +import app.notesr.R; +import app.notesr.activity.ActivityBase; +import app.notesr.activity.DialogFactory; +import app.notesr.data.model.Note; +import app.notesr.service.note.NoteService; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class SaveNoteOnClick implements MenuItem.OnMenuItemClickListener { + + private final ActivityBase activity; + private final Note note; + private final NoteService noteService; + private final DialogFactory dialogFactory; + private final EditText nameField; + private final EditText textField; + + @Override + public boolean onMenuItemClick(@NonNull MenuItem item) { + String name = nameField.getText().toString(); + String text = textField.getText().toString(); + + if (!name.isBlank() && !text.isBlank()) { + note.setName(name); + note.setText(text); + note.setUpdatedAt(LocalDateTime.now()); + + Dialog progressDialog = dialogFactory + .getThemedProgressDialog(R.layout.progress_dialog_loading); + + newSingleThreadExecutor().execute(() -> { + activity.runOnUiThread(progressDialog::show); + noteService.save(note); + + activity.runOnUiThread(() -> { + progressDialog.dismiss(); + activity.startActivity(new Intent(activity, NotesListActivity.class)); + }); + }); + } + + return true; + } +} diff --git a/app/src/main/res/drawable/ic_visibility.xml b/app/src/main/res/drawable/ic_visibility.xml new file mode 100644 index 00000000..2b9c462b --- /dev/null +++ b/app/src/main/res/drawable/ic_visibility.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/activity_open_note.xml b/app/src/main/res/layout/activity_open_note.xml index a4635b74..eacc1064 100644 --- a/app/src/main/res/layout/activity_open_note.xml +++ b/app/src/main/res/layout/activity_open_note.xml @@ -12,7 +12,7 @@ android:id="@+id/noteNameField" android:layout_width="match_parent" android:layout_height="53dp" - android:textSize="25sp" + android:textSize="30sp" android:backgroundTint="@color/activity_background" android:ems="10" android:hint="@string/name" @@ -44,6 +44,7 @@ android:hint="@string/start_typing" android:inputType="textMultiLine|textCapSentences" android:scrollbars="vertical" + android:textSize="18sp" android:textColor="@color/text_color" android:textColorHighlight="@color/select_text" android:textColorHint="#8F8F8F" @@ -61,4 +62,40 @@ app:layout_constraintTop_toBottomOf="@+id/noteNameField" app:layout_constraintVertical_bias="0.0" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_open_node_open_mode.xml b/app/src/main/res/menu/menu_open_node_open_mode.xml new file mode 100644 index 00000000..6e54983a --- /dev/null +++ b/app/src/main/res/menu/menu_open_node_open_mode.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_open_note.xml b/app/src/main/res/menu/menu_open_note.xml index f1982bb9..7b3a81a0 100644 --- a/app/src/main/res/menu/menu_open_note.xml +++ b/app/src/main/res/menu/menu_open_note.xml @@ -4,7 +4,7 @@ @@ -12,15 +12,22 @@ + + \ 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 2650c345..4e95a3df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,7 +32,7 @@ Name Start typing… New - Edit + Edit This action cannot be undone.\nAre you sure? YES NO @@ -104,4 +104,7 @@ (%1$s) Files of: %2$s Error Re-encryption failed, your key and your data remains unchanged. + View Text + Markdown View + Change open mode \ No newline at end of file