Skip to content

Commit

Permalink
Support direct editing for notes (#1686)
Browse files Browse the repository at this point in the history
* feat(capabilities): Fetch and store direct editing capability using existing capabilities code

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* feat: Implement direct editing repo

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* wip: Edit note with webview

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* wip: allow switching between the three note opening modes in preferences

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* EditNoteActivity: if no setting, use plain edit, not direct edit

Required by UX team

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* feat: Add FAB to switch to rich editing mode from plain edit/preview

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* feat: add fab while direct editing to switch to normal editing

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Fix toolbar when switching between direct edit and normal edit

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* wip: error and conflict handling when switching edit modes

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Only show direct editing FAB if direct editing is available

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* EditNoteActivity: if pref is direct edit but it's not available, launch normal edit instead

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Show error if direct editing not loaded after 10 seconds

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Update user agent for Notes webview

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Support opening new notes with direct editing

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Allow invalid ssl certs for debug builds in webview

Development only!

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* NoteDirectEdit: prevent duplicate note creation when creating it with direct edit

remote id needs to be set

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

* Fix create with plain edit -> direct edit flow

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>

---------

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
  • Loading branch information
AlvaroBrey authored Mar 7, 2023
1 parent 83c91cc commit 4e096b4
Show file tree
Hide file tree
Showing 37 changed files with 1,156 additions and 98 deletions.
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ dependencies {

// ReactiveX
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'

// Testing
testImplementation 'androidx.test:core:1.5.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.app.Application;
import android.content.Context;
import android.util.Log;
import android.webkit.WebView;

import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
Expand All @@ -29,6 +30,9 @@ public void onCreate() {
lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false);
isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false);
super.onCreate();
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true);
}
}

public static void setAppTheme(DarkModeSetting setting) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
private Note originalNote;
private int originalScrollY;
protected NotesRepository repo;
private NoteFragmentListener listener;
@Nullable
protected NoteFragmentListener listener;
private boolean titleModified = false;

protected boolean isNew = true;
Expand Down Expand Up @@ -143,6 +144,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
@Nullable
protected abstract ScrollView getScrollView();


protected abstract void scrollToY(int scrollY);

@Override
Expand Down Expand Up @@ -240,7 +242,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
.show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName()));
return true;
} else if (itemId == R.id.menu_share) {
ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
shareNote();
return false;
} else if (itemId == MENU_ID_PIN) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Expand All @@ -263,6 +265,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}

protected void shareNote() {
ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
}

@CallSuper
protected void onNoteLoaded(Note note) {
this.originalScrollY = note.getScrollY();
Expand All @@ -273,10 +279,21 @@ protected void onNoteLoaded(Note note) {
if (scrollY > 0) {
note.setScrollY(scrollY);
}
onScroll(scrollY, oldScrollY);
});
}
}

/**
* Scroll callback, to be overridden by subclasses. Default implementation is empty
*/
protected void onScroll(int scrollY, int oldScrollY) {
}

protected boolean shouldShowToolbar() {
return true;
}

public void onCloseNote() {
if (!titleModified && originalNote == null && getContent().isEmpty()) {
repo.deleteNoteAndSync(localAccount, note.getId());
Expand Down Expand Up @@ -367,8 +384,14 @@ public void moveNote(Account account) {
}

public interface NoteFragmentListener {
enum Mode {
EDIT, PREVIEW, DIRECT_EDIT
}

void close();

void onNoteUpdated(Note note);

void changeMode(@NonNull Mode mode, boolean reloadNote);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;

import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;

import java.io.BufferedReader;
import java.io.IOException;
Expand All @@ -34,6 +37,7 @@
import it.niedermann.owncloud.notes.databinding.ActivityEditBinding;
import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
import it.niedermann.owncloud.notes.main.MainActivity;
import it.niedermann.owncloud.notes.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
Expand All @@ -57,11 +61,14 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
private ActivityEditBinding binding;

private BaseNoteFragment fragment;
private NotesRepository repo;

@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

repo = NotesRepository.getInstance(getApplicationContext());

try {
if (SingleAccountHelper.getCurrentSingleSignOnAccount(this) == null) {
throw new NoCurrentAccountSelectedException();
Expand Down Expand Up @@ -118,9 +125,20 @@ private long getNoteId() {
}

private long getAccountId() {
return getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0);
final long idParam = getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0);
if (idParam == 0) {
try {
final SingleSignOnAccount ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(this);
return repo.getAccountByName(ssoAcc.name).getId();
} catch (NextcloudFilesAppAccountNotFoundException |
NoCurrentAccountSelectedException e) {
Log.w(TAG, "getAccountId: no current account", e);
}
}
return idParam;
}


/**
* Starts the note fragment for an existing note or a new note.
* The actual behavior is triggered by the activity's intent.
Expand All @@ -145,44 +163,109 @@ private void launchNoteFragment() {
* @param noteId ID of the existing note.
*/
private void launchExistingNote(long accountId, long noteId) {
final var prefKeyNoteMode = getString(R.string.pref_key_note_mode);
final var prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
final var prefValueEdit = getString(R.string.pref_value_mode_edit);
final var prefValuePreview = getString(R.string.pref_value_mode_preview);
final var prefValueLast = getString(R.string.pref_value_mode_last);
launchExistingNote(accountId, noteId, null);
}

final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
final String mode = preferences.getString(prefKeyNoteMode, prefValueEdit);
final String lastMode = preferences.getString(prefKeyLastMode, prefValueEdit);
boolean editMode = true;
if (prefValuePreview.equals(mode) || (prefValueLast.equals(mode) && prefValuePreview.equals(lastMode))) {
editMode = false;
}
launchExistingNote(accountId, noteId, editMode);
private void launchExistingNote(long accountId, long noteId, @Nullable final String mode) {
launchExistingNote(accountId, noteId, mode, false);
}

/**
* Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note.
*
* @param noteId ID of the existing note.
* @param edit View-mode of the fragment:
* <code>true</code> for {@link NoteEditFragment},
* <code>false</code> for {@link NotePreviewFragment}.
* @param noteId ID of the existing note.
* @param mode View-mode of the fragment (pref value or null). If null will be chosen based on
* user preferences.
* @param discardState If true, the state of the fragment will be discarded and a new fragment will be created
*/
private void launchExistingNote(long accountId, long noteId, boolean edit) {
private void launchExistingNote(long accountId, long noteId, @Nullable final String mode, final boolean discardState) {
// save state of the fragment in order to resume with the same note and originalNote
Fragment.SavedState savedState = null;
if (fragment != null) {
savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
runOnUiThread(() -> {
Fragment.SavedState savedState = null;
if (fragment != null && !discardState) {
savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
}
fragment = getNoteFragment(accountId, noteId, mode);
if (savedState != null) {
fragment.setInitialSavedState(savedState);
}
replaceFragment();
});
}

private void replaceFragment() {
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
if (!fragment.shouldShowToolbar()) {
binding.toolbar.setVisibility(View.GONE);
} else {
binding.toolbar.setVisibility(View.VISIBLE);
}
fragment = edit
? NoteEditFragment.newInstance(accountId, noteId)
: NotePreviewFragment.newInstance(accountId, noteId);
}


/**
* Returns the preferred mode for the account. If the mode is "remember last" the last mode is returned.
* If the mode is "direct edit" and the account does not support direct edit, the default mode is returned.
*/
private String getPreferenceMode(long accountId) {

final var prefKeyNoteMode = getString(R.string.pref_key_note_mode);
final var prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
final var defaultMode = getString(R.string.pref_value_mode_edit);
final var prefValueLast = getString(R.string.pref_value_mode_last);
final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);

if (savedState != null) {
fragment.setInitialSavedState(savedState);

final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
final String modePreference = preferences.getString(prefKeyNoteMode, defaultMode);

String effectiveMode = modePreference;
if (modePreference.equals(prefValueLast)) {
effectiveMode = preferences.getString(prefKeyLastMode, defaultMode);
}

if (effectiveMode.equals(prefValueDirectEdit)) {
final Account accountById = repo.getAccountById(accountId);
final var directEditAvailable = accountById != null && accountById.isDirectEditingAvailable();
if (!directEditAvailable) {
effectiveMode = defaultMode;
}
}

return effectiveMode;
}

private BaseNoteFragment getNoteFragment(long accountId, long noteId, final @Nullable String modePref) {

final var effectiveMode = modePref == null ? getPreferenceMode(accountId) : modePref;

final var prefValueEdit = getString(R.string.pref_value_mode_edit);
final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);
final var prefValuePreview = getString(R.string.pref_value_mode_preview);

if (effectiveMode.equals(prefValueEdit)) {
return NoteEditFragment.newInstance(accountId, noteId);
} else if (effectiveMode.equals(prefValueDirectEdit)) {
return NoteDirectEditFragment.newInstance(accountId, noteId);
} else if (effectiveMode.equals(prefValuePreview)) {
return NotePreviewFragment.newInstance(accountId, noteId);
} else {
throw new IllegalStateException("Unknown note modePref: " + modePref);
}
}


@NonNull
private BaseNoteFragment getNewNoteFragment(Note newNote) {
final var mode = getPreferenceMode(getAccountId());

final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);

if (mode.equals(prefValueDirectEdit)) {
return NoteDirectEditFragment.newInstanceWithNewNote(newNote);
} else {
return NoteEditFragment.newInstanceWithNewNote(newNote);
}
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
}

/**
Expand Down Expand Up @@ -219,10 +302,11 @@ private void launchNewNote() {
content = "";
}
final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null);
fragment = NoteEditFragment.newInstanceWithNewNote(newNote);
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
fragment = getNewNoteFragment(newNote);
replaceFragment();
}


private void launchReadonlyNote() {
final var intent = getIntent();
final var content = new StringBuilder();
Expand All @@ -238,7 +322,7 @@ private void launchReadonlyNote() {
}

fragment = NoteReadonlyFragment.newInstance(content.toString());
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
replaceFragment();
}

@Override
Expand All @@ -260,10 +344,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
close();
return true;
} else if (itemId == R.id.menu_preview) {
launchExistingNote(getAccountId(), getNoteId(), false);
changeMode(Mode.PREVIEW, false);
return true;
} else if (itemId == R.id.menu_edit) {
launchExistingNote(getAccountId(), getNoteId(), true);
changeMode(Mode.EDIT, false);
return true;
}
return super.onOptionsItemSelected(item);
Expand All @@ -281,8 +365,10 @@ public void close() {
final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
if (fragment instanceof NoteEditFragment) {
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_edit)).apply();
} else {
} else if (fragment instanceof NotePreviewFragment) {
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_preview)).apply();
} else if (fragment instanceof NoteDirectEditFragment) {
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_direct_edit)).apply();
}
fragment.onCloseNote();

Expand All @@ -308,6 +394,24 @@ public void onNoteUpdated(Note note) {
}
}

@Override
public void changeMode(@NonNull Mode mode, boolean reloadNote) {
switch (mode) {
case EDIT:
launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_edit), reloadNote);
break;
case PREVIEW:
launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_preview), reloadNote);
break;
case DIRECT_EDIT:
launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_direct_edit), reloadNote);
break;
default:
throw new IllegalStateException("Unknown mode: " + mode);
}
}


@Override
public void onAccountPicked(@NonNull Account account) {
fragment.moveNote(account);
Expand All @@ -318,4 +422,4 @@ public void applyBrand(int color) {
final var util = BrandingUtil.of(color, this);
util.notes.applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar, colorAccent);
}
}
}
Loading

0 comments on commit 4e096b4

Please sign in to comment.