From bbf1a7da50e9b9fc4a5749bf6af1799afa511f87 Mon Sep 17 00:00:00 2001 From: Stefan Niedermann Date: Wed, 23 Jun 2021 16:02:46 +0200 Subject: [PATCH] #1277 Redesign note details Signed-off-by: Stefan Niedermann --- .../owncloud/notes/branding/BrandingUtil.java | 13 ++ .../owncloud/notes/edit/BaseNoteFragment.java | 22 +-- .../owncloud/notes/edit/EditNoteActivity.java | 4 +- .../notes/edit/category/CategoryAdapter.java | 134 ---------------- .../edit/category/CategoryDialogFragment.java | 2 + .../notes/edit/details/CategoryAdapter.java | 110 +++++++++++++ .../CategoryViewModel.java | 5 +- .../details/NoteDetailsDialogFragment.java | 148 +++++++++++++----- .../edit/details/NoteDetailsViewModel.java | 43 +++-- .../edit/title/EditTitleDialogFragment.java | 96 ------------ .../owncloud/notes/main/MainActivity.java | 4 +- .../notes/persistence/NotesRepository.java | 35 ++++- .../notes/persistence/dao/NoteDao.java | 13 ++ .../main/res/layout/dialog_note_details.xml | 118 +++++++------- app/src/main/res/values/strings.xml | 1 + 15 files changed, 386 insertions(+), 362 deletions(-) delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/details/CategoryAdapter.java rename app/src/main/java/it/niedermann/owncloud/notes/edit/{category => details}/CategoryViewModel.java (87%) delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java index bc0faca77..ae44f4b3f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java @@ -21,6 +21,8 @@ import androidx.lifecycle.MediatorLiveData; import androidx.preference.PreferenceManager; +import com.google.android.material.textfield.TextInputLayout; + import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData; import it.niedermann.owncloud.notes.NotesApplication; import it.niedermann.owncloud.notes.R; @@ -161,4 +163,15 @@ public static void applyBrandToLayerDrawable(@NonNull LayerDrawable check, @IdRe DrawableCompat.setTint(drawable, mainColor); } } + + public static void applyBrandToEditTextInputLayout(@ColorInt int color, @NonNull TextInputLayout til) { + final int colorPrimary = ContextCompat.getColor(til.getContext(), R.color.primary); + final int colorAccent = ContextCompat.getColor(til.getContext(), R.color.accent); + final ColorStateList colorDanger = ColorStateList.valueOf(ContextCompat.getColor(til.getContext(), R.color.design_default_color_error)); + til.setBoxStrokeColor(contrastRatioIsSufficient(color, colorPrimary) ? color : colorAccent); + til.setHintTextColor(ColorStateList.valueOf(contrastRatioIsSufficient(color, colorPrimary) ? color : colorAccent)); + til.setErrorTextColor(colorDanger); + til.setBoxStrokeErrorColor(colorDanger); + til.setErrorIconTintList(colorDanger); + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index 3f0dff698..c5e6a9f66 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -35,11 +35,7 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment; import it.niedermann.owncloud.notes.branding.BrandedFragment; -import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; -import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment.CategoryDialogListener; import it.niedermann.owncloud.notes.edit.details.NoteDetailsDialogFragment; -import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment; -import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment.EditTitleListener; import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; @@ -47,11 +43,10 @@ import it.niedermann.owncloud.notes.shared.model.ISyncCallback; import it.niedermann.owncloud.notes.shared.util.NoteUtil; import it.niedermann.owncloud.notes.shared.util.NotesColorUtil; -import it.niedermann.owncloud.notes.shared.util.ShareUtil; import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive; -public abstract class BaseNoteFragment extends BrandedFragment implements CategoryDialogListener, EditTitleListener { +public abstract class BaseNoteFragment extends BrandedFragment implements NoteDetailsDialogFragment.NoteDetailsListener { private static final String TAG = BaseNoteFragment.class.getSimpleName(); protected final ExecutorService executor = Executors.newCachedThreadPool(); @@ -284,22 +279,13 @@ public void onScheduled() { } @Override - public void onCategoryChosen(String category) { - repo.setCategory(localAccount, note.getId(), category); + public void onNoteDetailsEdited(String title, String category) { + titleModified = true; + note.setTitle(title); note.setCategory(category); listener.onNoteUpdated(note); } - @Override - public void onTitleEdited(String newTitle) { - titleModified = true; - note.setTitle(newTitle); - executor.submit(() -> { - note = repo.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null); - requireActivity().runOnUiThread(() -> listener.onNoteUpdated(note)); - }); - } - public void moveNote(Account account) { final LiveData moveLiveData = repo.moveNoteToAnotherAccount(account, note); moveLiveData.observe(this, (v) -> moveLiveData.removeObservers(this)); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java index a1eb8d68f..4b54c53c1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java @@ -29,11 +29,9 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener; import it.niedermann.owncloud.notes.databinding.ActivityEditBinding; -import it.niedermann.owncloud.notes.databinding.ActivityEditBinding; -import it.niedermann.owncloud.notes.edit.category.CategoryViewModel; +import it.niedermann.owncloud.notes.edit.details.CategoryViewModel; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; -import it.niedermann.owncloud.notes.shared.model.DBStatus; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; import it.niedermann.owncloud.notes.shared.util.NoteUtil; import it.niedermann.owncloud.notes.shared.util.ShareUtil; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java deleted file mode 100644 index 4b3bb9ba8..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java +++ /dev/null @@ -1,134 +0,0 @@ -package it.niedermann.owncloud.notes.edit.category; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.ItemCategoryBinding; -import it.niedermann.owncloud.notes.main.navigation.NavigationItem; -import it.niedermann.owncloud.notes.shared.util.NoteUtil; - -public class CategoryAdapter extends RecyclerView.Adapter { - - private static final String clearItemId = "clear_item"; - private static final String addItemId = "add_item"; - @NonNull - private List categories = new ArrayList<>(); - @NonNull - private final CategoryListener listener; - private final Context context; - - CategoryAdapter(@NonNull Context context, @NonNull CategoryListener categoryListener) { - this.context = context; - this.listener = categoryListener; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); - return new CategoryViewHolder(v); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - NavigationItem category = categories.get(position); - CategoryViewHolder categoryViewHolder = (CategoryViewHolder) holder; - - switch (category.id) { - case addItemId: - Drawable wrapDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(context, category.icon)); - DrawableCompat.setTint(wrapDrawable, ContextCompat.getColor(context, R.color.icon_color_default)); - categoryViewHolder.getIcon().setImageDrawable(wrapDrawable); - categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryAdded()); - break; - case clearItemId: - categoryViewHolder.getIcon().setImageDrawable(ContextCompat.getDrawable(context, category.icon)); - categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryCleared()); - break; - default: - categoryViewHolder.getIcon().setImageDrawable(ContextCompat.getDrawable(context, category.icon)); - categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryChosen(category.label)); - break; - } - categoryViewHolder.getCategory().setText(NoteUtil.extendCategory(category.label)); - if (category.count != null && category.count > 0) { - categoryViewHolder.getCount().setText(String.valueOf(category.count)); - } else { - categoryViewHolder.getCount().setVisibility(View.GONE); - } - } - - @Override - public int getItemCount() { - return categories.size(); - } - - static class CategoryViewHolder extends RecyclerView.ViewHolder { - private final ItemCategoryBinding binding; - - private CategoryViewHolder(View view) { - super(view); - binding = ItemCategoryBinding.bind(view); - } - - private View getCategoryWrapper() { - return binding.categoryWrapper; - } - - private AppCompatImageView getIcon() { - return binding.icon; - } - - private TextView getCategory() { - return binding.category; - } - - private TextView getCount() { - return binding.count; - } - } - - void setCategoryList(List categories, @Nullable String currentSearchString) { - this.categories.clear(); - this.categories.addAll(categories); - final NavigationItem clearItem = new NavigationItem(clearItemId, context.getString(R.string.no_category), 0, R.drawable.ic_clear_grey_24dp); - this.categories.add(0, clearItem); - if (currentSearchString != null && currentSearchString.trim().length() > 0) { - boolean currentSearchStringIsInCategories = false; - for (NavigationItem category : categories) { - if (currentSearchString.equals(category.label)) { - currentSearchStringIsInCategories = true; - break; - } - } - if (!currentSearchStringIsInCategories) { - NavigationItem addItem = new NavigationItem(addItemId, context.getString(R.string.add_category, currentSearchString.trim()), 0, R.drawable.ic_add_blue_24dp); - this.categories.add(addItem); - } - } - notifyDataSetChanged(); - } - - public interface CategoryListener { - void onCategoryChosen(String category); - - void onCategoryAdded(); - - void onCategoryCleared(); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java index 648578d42..c283ae725 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java @@ -24,6 +24,8 @@ import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.DialogChangeCategoryBinding; +import it.niedermann.owncloud.notes.edit.details.CategoryAdapter; +import it.niedermann.owncloud.notes.edit.details.CategoryViewModel; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/details/CategoryAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/CategoryAdapter.java new file mode 100644 index 000000000..1f64a0f5d --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/CategoryAdapter.java @@ -0,0 +1,110 @@ +package it.niedermann.owncloud.notes.edit.details; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.databinding.ItemCategoryBinding; +import it.niedermann.owncloud.notes.main.navigation.NavigationItem; +import it.niedermann.owncloud.notes.shared.util.NoteUtil; + +public class CategoryAdapter extends RecyclerView.Adapter { + + @NonNull + private final List categories = new ArrayList<>(); + @NonNull + private final CategoryListener listener; + private final Context context; + + + public CategoryAdapter(@NonNull Context context, @NonNull CategoryListener categoryListener) { + this.context = context; + this.listener = categoryListener; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false); + return new CategoryViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + final NavigationItem category = categories.get(position); + final CategoryViewHolder categoryViewHolder = (CategoryViewHolder) holder; + categoryViewHolder.getIcon().setImageDrawable(ContextCompat.getDrawable(context, category.icon)); + categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryChosen(category.label)); + categoryViewHolder.getCategory().setText(NoteUtil.extendCategory(category.label)); + if (category.count != null && category.count > 0) { + categoryViewHolder.getCount().setText(String.valueOf(category.count)); + } else { + categoryViewHolder.getCount().setVisibility(View.GONE); + } + } + + @Override + public int getItemCount() { + return categories.size(); + } + + static class CategoryViewHolder extends RecyclerView.ViewHolder { + private final ItemCategoryBinding binding; + + private CategoryViewHolder(View view) { + super(view); + binding = ItemCategoryBinding.bind(view); + } + + private View getCategoryWrapper() { + return binding.categoryWrapper; + } + + private AppCompatImageView getIcon() { + return binding.icon; + } + + private TextView getCategory() { + return binding.category; + } + + private TextView getCount() { + return binding.count; + } + } + + /** + * @deprecated use {@link #setCategoryList(List)} + */ + public void setCategoryList(List categories, @Nullable String currentSearchString) { + setCategoryList(categories); + } + + public void setCategoryList(@NonNull List categories) { + this.categories.clear(); + this.categories.addAll(categories); + notifyDataSetChanged(); + } + + public interface CategoryListener { + void onCategoryChosen(String category); + + default void onCategoryAdded() { + } + + default void onCategoryCleared() { + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/CategoryViewModel.java similarity index 87% rename from app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java rename to app/src/main/java/it/niedermann/owncloud/notes/edit/details/CategoryViewModel.java index ea5efd37a..4a18026e9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/CategoryViewModel.java @@ -1,4 +1,4 @@ -package it.niedermann.owncloud.notes.edit.category; +package it.niedermann.owncloud.notes.edit.details; import android.app.Application; import android.text.TextUtils; @@ -13,6 +13,7 @@ import it.niedermann.owncloud.notes.main.navigation.NavigationItem; import it.niedermann.owncloud.notes.persistence.NotesRepository; +import static androidx.lifecycle.Transformations.distinctUntilChanged; import static androidx.lifecycle.Transformations.map; import static androidx.lifecycle.Transformations.switchMap; import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.convertToCategoryNavigationItem; @@ -35,7 +36,7 @@ public void postSearchTerm(@NonNull String searchTerm) { @NonNull public LiveData> getCategories(long accountId) { - return switchMap(this.searchTerm, searchTerm -> + return switchMap(distinctUntilChanged(this.searchTerm), searchTerm -> map(repo.searchCategories$(accountId, TextUtils.isEmpty(searchTerm) ? "%" : "%" + searchTerm + "%"), categories -> convertToCategoryNavigationItem(getApplication(), categories))); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsDialogFragment.java index 285e72aff..b6bf96c3d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsDialogFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsDialogFragment.java @@ -10,6 +10,9 @@ import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; import android.text.format.DateUtils; import android.util.Log; import android.view.View; @@ -19,11 +22,16 @@ import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; +import java.util.stream.Collectors; + import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.DialogNoteDetailsBinding; import it.niedermann.owncloud.notes.edit.EditNoteActivity; import it.niedermann.owncloud.notes.persistence.entity.Account; +import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.shared.model.ApiVersion; +import it.niedermann.owncloud.notes.shared.model.IResponseCallback; import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; import it.niedermann.owncloud.notes.shared.util.ShareUtil; @@ -34,14 +42,17 @@ public class NoteDetailsDialogFragment extends DialogFragment { private static final String TAG = NoteDetailsDialogFragment.class.getSimpleName(); - static final String PARAM_ACCOUNT = "account"; - static final String PARAM_NOTE_ID = "noteId"; + private static final String PARAM_ACCOUNT = "account"; + private static final String PARAM_NOTE_ID = "noteId"; + private DialogNoteDetailsBinding binding; + private NoteDetailsViewModel viewModel; + private CategoryViewModel categoryViewModel; + private NoteDetailsListener listener; private Account account; private long noteId; - private NoteDetailsViewModel viewModel; @Override public void onAttach(@NonNull Context context) { @@ -49,7 +60,17 @@ public void onAttach(@NonNull Context context) { final Bundle args = requireArguments(); account = (Account) args.getSerializable(PARAM_ACCOUNT); noteId = args.getLong(PARAM_NOTE_ID); + + if (getTargetFragment() instanceof NoteDetailsListener) { + listener = (NoteDetailsListener) getTargetFragment(); + } else if (getActivity() instanceof NoteDetailsListener) { + listener = (NoteDetailsListener) getActivity(); + } else { + throw new IllegalArgumentException("Calling activity or target fragment must implement " + NoteDetailsListener.class.getSimpleName()); + } + viewModel = new ViewModelProvider(requireActivity()).get(NoteDetailsViewModel.class); + categoryViewModel = new ViewModelProvider(requireActivity()).get(CategoryViewModel.class); } @NonNull @@ -60,10 +81,16 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { final Dialog dialog = new AlertDialog.Builder(getActivity()) .setView(binding.getRoot()) .setCancelable(true) - .setNegativeButton(R.string.simple_close, null) + .setPositiveButton(R.string.action_edit_save, (d, w) -> { + listener.onNoteDetailsEdited(binding.title.getText().toString(), binding.category.getText().toString()); + viewModel.commit(account, noteId, binding.title.getText().toString(), binding.category.getText().toString()); + }) + .setNeutralButton(android.R.string.cancel, null) .create(); dialog.setOnShowListener((d) -> { - if (!(isRequestPinShortcutSupported(requireActivity()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)) { + BrandingUtil.applyBrandToEditTextInputLayout(account.getColor(), binding.titleWrapper); + BrandingUtil.applyBrandToEditTextInputLayout(account.getColor(), binding.categoryWrapper); + if (!isRequestPinShortcutSupported(requireContext()) || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { binding.pin.setVisibility(View.GONE); } final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(account.getApiVersion()); @@ -71,44 +98,89 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { if (!supportsTitle) { binding.titleWrapper.setVisibility(View.GONE); } + binding.category.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Nothing to do here... + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + categoryViewModel.postSearchTerm(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + // Nothing to do here... + } + }); - viewModel.getNote$(noteId).observe(this, (note) -> { - binding.favorite.setImageResource(note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp); - if (supportsTitle) { - binding.title.setText(note.getTitle()); + final CategoryAdapter adapter = new CategoryAdapter(requireContext(), category -> binding.category.setText(category)); + binding.categories.setAdapter(adapter); + + viewModel.isFavorite$(noteId).observe(this, (isFavorite) -> binding.favorite.setImageResource(isFavorite ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp)); + viewModel.getCategory$(noteId).observe(this, (category) -> { + if (!TextUtils.equals(binding.category.getText(), category)) { + binding.category.setText(category); + } + categoryViewModel.postSearchTerm(category); + }); + categoryViewModel.getCategories(account.getId()).observe(this, categories -> adapter.setCategoryList(categories.stream().filter(category -> !TextUtils.equals(category.category, binding.category.getText())).collect(Collectors.toList()))); + viewModel.getTitle$(noteId).observe(this, (title) -> { + if (!TextUtils.equals(binding.title.getText(), title)) { + binding.title.setText(title); } - binding.category.setText(note.getCategory()); - binding.modified.setText(DateUtils.getRelativeDateTimeString( - getContext(), - note.getModified().getTimeInMillis(), - DateUtils.SECOND_IN_MILLIS, - DateUtils.WEEK_IN_MILLIS, - 0 - )); - - binding.pin.setOnClickListener((v) -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - final ShortcutManager shortcutManager = requireActivity().getSystemService(ShortcutManager.class); - if (shortcutManager != null) { - if (shortcutManager.isRequestPinShortcutSupported()) { - final ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(getActivity(), String.valueOf(note.getId())) - .setShortLabel(note.getTitle()) - .setIcon(Icon.createWithResource(requireActivity().getApplicationContext(), TRUE.equals(note.getFavorite()) ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp)) - .setIntent(new Intent(getActivity(), EditNoteActivity.class).putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()).setAction(ACTION_SHORTCUT)) - .build(); - - shortcutManager.requestPinShortcut(pinShortcutInfo, PendingIntent.getBroadcast(getActivity(), 0, shortcutManager.createShortcutResultIntent(pinShortcutInfo), 0).getIntentSender()); - } else { - Log.i(TAG, "RequestPinShortcut is not supported"); - } + }); + viewModel.getModified$(noteId).observe(this, (modified) -> binding.modified.setText(DateUtils.getRelativeDateTimeString( + getContext(), + modified.getTimeInMillis(), + DateUtils.SECOND_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0 + ))); + binding.pin.setOnClickListener((v) -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + final ShortcutManager shortcutManager = requireActivity().getSystemService(ShortcutManager.class); + if (shortcutManager != null) { + if (shortcutManager.isRequestPinShortcutSupported()) { + viewModel.getNoteById(noteId, new IResponseCallback() { + @Override + public void onSuccess(Note note) { + final ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(getActivity(), String.valueOf(note.getId())) + .setShortLabel(note.getTitle()) + .setIcon(Icon.createWithResource(requireActivity().getApplicationContext(), TRUE.equals(note.getFavorite()) ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp)) + .setIntent(new Intent(getActivity(), EditNoteActivity.class).putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()).setAction(ACTION_SHORTCUT)) + .build(); + shortcutManager.requestPinShortcut(pinShortcutInfo, PendingIntent.getBroadcast(getActivity(), 0, shortcutManager.createShortcutResultIntent(pinShortcutInfo), 0).getIntentSender()); + } + + @Override + public void onError(@NonNull Throwable t) { + requireActivity().runOnUiThread(NoteDetailsDialogFragment.this::dismiss); + } + }); } else { - Log.e(TAG, ShortcutManager.class.getSimpleName() + " is null"); + Log.i(TAG, "RequestPinShortcut is not supported"); } + } else { + Log.e(TAG, ShortcutManager.class.getSimpleName() + " is null"); + } + } + }); + binding.share.setOnClickListener((v) -> { + viewModel.getNoteById(noteId, new IResponseCallback() { + @Override + public void onSuccess(Note note) { + ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent()); + } + + @Override + public void onError(@NonNull Throwable t) { + requireActivity().runOnUiThread(NoteDetailsDialogFragment.this::dismiss); } }); - binding.share.setOnClickListener((v) -> ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent())); - binding.favorite.setOnClickListener((v) -> viewModel.toggleFavorite(noteId)); }); + binding.favorite.setOnClickListener((v) -> viewModel.toggleFavorite(account, noteId)); }); return dialog; } @@ -121,4 +193,8 @@ public static DialogFragment newInstance(@NonNull Account account, long noteId) fragment.setArguments(args); return fragment; } + + public interface NoteDetailsListener { + void onNoteDetailsEdited(String title, String category); + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsViewModel.java index 5306bf0d2..028ff0fda 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsViewModel.java @@ -8,37 +8,58 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.SavedStateHandle; +import java.time.Instant; +import java.util.Calendar; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; 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.IResponseCallback; public class NoteDetailsViewModel extends AndroidViewModel { - private static final String TAG = NoteDetailsViewModel.class.getSimpleName(); - private final ExecutorService executor = Executors.newCachedThreadPool(); - private final SavedStateHandle state; - @NonNull private final NotesRepository repo; - - public NoteDetailsViewModel(@NonNull Application application, @NonNull SavedStateHandle savedStateHandle) { + public NoteDetailsViewModel(@NonNull Application application) { super(application); this.repo = NotesRepository.getInstance(application); - this.state = savedStateHandle; } - public LiveData getNote$(long noteId) { - return repo.getNoteById$(noteId); + public void getNoteById(long noteId, @NonNull IResponseCallback callback) { + executor.submit(() -> callback.onSuccess(repo.getNoteById(noteId))); + } + + public LiveData getTitle$(long noteId) { + return repo.getTitle$(noteId); + } + + public LiveData getModified$(long noteId) { + return repo.getModified$(noteId); + } + + public LiveData getCategory$(long noteId) { + return repo.getCategory$(noteId); + } + + public LiveData isFavorite$(long noteId) { + return repo.isFavorite$(noteId); } @AnyThread - public void toggleFavorite(long noteId) { - repo.toggleFavorite(noteId); + public void toggleFavorite(@NonNull Account account, long noteId) { + repo.toggleFavoriteAndSync(account, noteId); + } + + public void commit(@NonNull Account account, long noteId, String title, String category) { + executor.submit(() -> { + final Note note = repo.getNoteById(noteId); + note.setCategory(category); + repo.updateNoteAndSync(account, note, note.getContent(), title, null); + }); } } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java deleted file mode 100644 index e9473399e..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java +++ /dev/null @@ -1,96 +0,0 @@ -package it.niedermann.owncloud.notes.edit.title; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.databinding.DialogEditTitleBinding; - -public class EditTitleDialogFragment extends DialogFragment { - - private static final String TAG = EditTitleDialogFragment.class.getSimpleName(); - static final String PARAM_OLD_TITLE = "old_title"; - private DialogEditTitleBinding binding; - - private String oldTitle; - private EditTitleListener listener; - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - final Bundle args = getArguments(); - if (args == null) { - throw new IllegalArgumentException("Provide at least " + PARAM_OLD_TITLE); - } - oldTitle = args.getString(PARAM_OLD_TITLE); - - if (getTargetFragment() instanceof EditTitleListener) { - listener = (EditTitleListener) getTargetFragment(); - } else if (getActivity() instanceof EditTitleListener) { - listener = (EditTitleListener) getActivity(); - } else { - throw new IllegalArgumentException("Calling activity or target fragment must implement " + EditTitleListener.class.getSimpleName()); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - View dialogView = View.inflate(getContext(), R.layout.dialog_edit_title, null); - binding = DialogEditTitleBinding.bind(dialogView); - - if (savedInstanceState == null) { - binding.title.setText(oldTitle); - } - - return new AlertDialog.Builder(getActivity()) - .setTitle(R.string.change_note_title) - .setView(dialogView) - .setCancelable(true) - .setPositiveButton(R.string.action_edit_save, (dialog, which) -> listener.onTitleEdited(binding.title.getText().toString())) - .setNegativeButton(R.string.simple_cancel, null) - .create(); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - binding.title.requestFocus(); - Window window = requireDialog().getWindow(); - if (window != null) { - window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - } else { - Log.w(TAG, "can not enable soft keyboard because " + Window.class.getSimpleName() + " is null."); - } - } - - public static DialogFragment newInstance(String title) { - final DialogFragment fragment = new EditTitleDialogFragment(); - final Bundle args = new Bundle(); - args.putString(PARAM_OLD_TITLE, title); - fragment.setArguments(args); - return fragment; - } - - /** - * Interface that must be implemented by the calling Activity. - */ - public interface EditTitleListener { - /** - * This method is called after the user has changed the title of a note manually. - * - * @param newTitle the new title that a user submitted - */ - void onTitleEdited(String newTitle); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 23e2ea423..baa159f58 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -15,7 +15,6 @@ import android.view.View; import android.view.ViewTreeObserver; import android.widget.LinearLayout; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -56,7 +55,6 @@ import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.Function; import java.util.stream.Collectors; import it.niedermann.owncloud.notes.LockedActivity; @@ -69,7 +67,7 @@ import it.niedermann.owncloud.notes.databinding.DrawerLayoutBinding; import it.niedermann.owncloud.notes.edit.EditNoteActivity; import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; -import it.niedermann.owncloud.notes.edit.category.CategoryViewModel; +import it.niedermann.owncloud.notes.edit.details.CategoryViewModel; import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java index 00b6928df..b52e59cbc 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java @@ -30,6 +30,7 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; +import java.time.Instant; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; @@ -231,6 +232,22 @@ public void updateModified(long id, long modified) { return db.getNoteDao().getNoteById$(id); } + public LiveData getTitle$(long noteId) { + return distinctUntilChanged(db.getNoteDao().getTitle$(noteId)); + } + + public LiveData getCategory$(long noteId) { + return distinctUntilChanged(db.getNoteDao().getCategory$(noteId)); + } + + public LiveData isFavorite$(long noteId) { + return distinctUntilChanged(db.getNoteDao().isFavorite$(noteId)); + } + + public LiveData getModified$(long noteId) { + return distinctUntilChanged(db.getNoteDao().getModified$(noteId)); + } + public Note getNoteById(long id) { return db.getNoteDao().getNoteById(id); } @@ -426,12 +443,10 @@ public Map getIdMap(long accountId) { @AnyThread public void toggleFavoriteAndSync(Account account, long noteId) { - toggleFavorite(noteId); - executor.submit(() -> scheduleSync(account, true)); - } - - public void toggleFavorite(long noteId) { - executor.submit(() -> db.getNoteDao().toggleFavorite(noteId)); + executor.submit(() -> { + db.getNoteDao().toggleFavorite(noteId); + scheduleSync(account, true); + }); } /** @@ -452,6 +467,14 @@ public void setCategory(@NonNull Account account, long noteId, @NonNull String c }); } + @AnyThread + public void setTitle(@NonNull Account account, long noteId, @NonNull String title) { + executor.submit(() -> { + final Note note = getNoteById(noteId); + updateNoteAndSync(account, note, note.getContent(), title, null); + }); + } + /** * Updates a single Note with a new content. * The title is derived from the new content automatically, and modified date as well as DBStatus are updated, too -- if the content differs to the state in the database. diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java index ca111727d..a86b93bf0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java @@ -7,6 +7,7 @@ import androidx.room.Query; import androidx.room.Update; +import java.util.Calendar; import java.util.List; import java.util.Set; @@ -53,6 +54,12 @@ public interface NoteDao { @Query(count) LiveData count$(long accountId); + @Query("SELECT title FROM NOTE WHERE id = :noteId") + LiveData getTitle$(long noteId); + + @Query("SELECT modified FROM NOTE WHERE id = :noteId") + LiveData getModified$(long noteId); + @Query(count) Integer count(long accountId); @@ -62,6 +69,9 @@ public interface NoteDao { @Query(countFavorites) Integer countFavorites(long accountId); + @Query("SELECT favorite FROM NOTE WHERE id = :noteId") + LiveData isFavorite$(long noteId); + @Query(searchRecentByModified) LiveData> searchRecentByModified$(long accountId, String query); @@ -188,6 +198,9 @@ public interface NoteDao { @Query("SELECT accountId, category, COUNT(*) as 'totalNotes' FROM NOTE WHERE STATUS != 'LOCAL_DELETED' AND accountId = :accountId GROUP BY category") LiveData> getCategories$(Long accountId); + @Query("SELECT category FROM NOTE WHERE id = :noteId") + LiveData getCategory$(long noteId); + @Query("SELECT accountId, category, COUNT(*) as 'totalNotes' FROM NOTE WHERE STATUS != 'LOCAL_DELETED' AND accountId = :accountId AND category != '' AND category LIKE :searchTerm GROUP BY category") LiveData> searchCategories$(Long accountId, String searchTerm); diff --git a/app/src/main/res/layout/dialog_note_details.xml b/app/src/main/res/layout/dialog_note_details.xml index d5d97d089..5c62aea93 100644 --- a/app/src/main/res/layout/dialog_note_details.xml +++ b/app/src/main/res/layout/dialog_note_details.xml @@ -5,62 +5,18 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - - - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + android:background="?attr/selectableItemBackground" + android:contentDescription="@string/label_favorites" + android:padding="@dimen/spacer_2x" + android:src="@drawable/ic_star_grey_ccc_24dp" + app:layout_constraintBottom_toBottomOf="@id/pin" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/share" + app:layout_constraintTop_toTopOf="@id/pin" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 007d7ed4b..4d181cfea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -299,4 +299,5 @@ Repair We detected an irrecoverably state of the app. Please backup your unsynchronized changes and clear the storage of the Notes app. Details + Title