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 4fc883ac3..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 @@ -1,12 +1,7 @@ package it.niedermann.owncloud.notes.edit; -import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; import android.graphics.Color; -import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; import android.util.Log; @@ -40,33 +35,22 @@ 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.title.EditTitleDialogFragment; -import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment.EditTitleListener; +import it.niedermann.owncloud.notes.edit.details.NoteDetailsDialogFragment; 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.ApiVersion; import it.niedermann.owncloud.notes.shared.model.DBStatus; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; -import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; 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 androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported; import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive; -import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon; -import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT; -import static java.lang.Boolean.TRUE; -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(); - protected static final int MENU_ID_PIN = -1; public static final String PARAM_NOTE_ID = "noteId"; public static final String PARAM_ACCOUNT_ID = "accountId"; public static final String PARAM_CONTENT = "content"; @@ -185,31 +169,9 @@ public void onSaveInstanceState(@NonNull Bundle outState) { public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { inflater.inflate(R.menu.menu_note_fragment, menu); - if (isRequestPinShortcutSupported(requireActivity()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - menu.add(Menu.NONE, MENU_ID_PIN, 110, R.string.pin_to_homescreen); - } - super.onCreateOptionsMenu(menu, inflater); } - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - if (note != null) { - prepareFavoriteOption(menu.findItem(R.id.menu_favorite)); - - final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion()); - menu.findItem(R.id.menu_title).setVisible(preferredApiVersion != null && preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) >= 0); - menu.findItem(R.id.menu_delete).setVisible(!isNew); - } - } - - private void prepareFavoriteOption(MenuItem item) { - item.setIcon(TRUE.equals(note.getFavorite()) ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp); - item.setChecked(note.getFavorite()); - tintMenuIcon(item, colorAccent); - } - /** * Main-Menu-Handler */ @@ -230,46 +192,11 @@ public boolean onOptionsItemSelected(MenuItem item) { repo.deleteNoteAndSync(localAccount, note.getId()); listener.close(); return true; - } else if (itemId == R.id.menu_favorite) { - repo.toggleFavoriteAndSync(localAccount, note.getId()); - listener.onNoteUpdated(note); - prepareFavoriteOption(item); - return true; - } else if (itemId == R.id.menu_category) { - showCategorySelector(); - return true; - } else if (itemId == R.id.menu_title) { - showEditTitleDialog(); - return true; } else if (itemId == R.id.menu_move) { executor.submit(() -> AccountPickerDialogFragment .newInstance(new ArrayList<>(repo.getAccounts()), note.getAccountId()) .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName())); return true; - } else if (itemId == R.id.menu_share) { - ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent()); - return false; - } else if (itemId == MENU_ID_PIN) { - 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(), 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"); - } - } else { - Log.e(TAG, ShortcutManager.class.getSimpleName() + " is null"); - } - } - - return true; } return super.onOptionsItemSelected(item); } @@ -312,6 +239,7 @@ protected void saveNote(@Nullable ISyncCallback callback) { } else { Log.v(TAG, "... not saving, since nothing has changed"); } + if (callback != null) callback.onScheduled(); } else { // FIXME requires database queries on main thread! note = repo.updateNoteAndSync(localAccount, note, newContent, null, callback); @@ -328,49 +256,34 @@ protected void saveNote(@Nullable ISyncCallback callback) { /** * Opens a dialog in order to chose a category */ - private void showCategorySelector() { - final String fragmentId = "fragment_category"; - FragmentManager manager = requireActivity().getSupportFragmentManager(); - Fragment frag = manager.findFragmentByTag(fragmentId); - if (frag != null) { - manager.beginTransaction().remove(frag).commit(); - } - final DialogFragment categoryFragment = CategoryDialogFragment.newInstance(note.getAccountId(), note.getCategory()); - categoryFragment.setTargetFragment(this, 0); - categoryFragment.show(manager, fragmentId); - } + public void showNoteDetailsDialog() { + saveNote(new ISyncCallback() { + @Override + public void onFinish() { - /** - * Opens a dialog in order to chose a category - */ - public void showEditTitleDialog() { - saveNote(null); - final String fragmentId = "fragment_edit_title"; - FragmentManager manager = requireActivity().getSupportFragmentManager(); - Fragment frag = manager.findFragmentByTag(fragmentId); - if (frag != null) { - manager.beginTransaction().remove(frag).commit(); - } - DialogFragment editTitleFragment = EditTitleDialogFragment.newInstance(note.getTitle()); - editTitleFragment.setTargetFragment(this, 0); - editTitleFragment.show(manager, fragmentId); - } + } - @Override - public void onCategoryChosen(String category) { - repo.setCategory(localAccount, note.getId(), category); - note.setCategory(category); - listener.onNoteUpdated(note); + @Override + public void onScheduled() { + final String fragmentId = "fragment_note_details"; + final FragmentManager manager = requireActivity().getSupportFragmentManager(); + Fragment frag = manager.findFragmentByTag(fragmentId); + if (frag != null) { + manager.beginTransaction().remove(frag).commit(); + } + DialogFragment noteDetailsFragment = NoteDetailsDialogFragment.newInstance(localAccount, note.getId()); + noteDetailsFragment.setTargetFragment(BaseNoteFragment.this, 0); + noteDetailsFragment.show(manager, fragmentId); + } + }); } @Override - public void onTitleEdited(String newTitle) { + public void onNoteDetailsEdited(String title, String category) { titleModified = true; - note.setTitle(newTitle); - executor.submit(() -> { - note = repo.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null); - requireActivity().runOnUiThread(() -> listener.onNoteUpdated(note)); - }); + note.setTitle(title); + note.setCategory(category); + listener.onNoteUpdated(note); } public void moveNote(Account account) { 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 c00db4aa0..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; @@ -84,7 +82,7 @@ protected void onCreate(final Bundle savedInstanceState) { } setSupportActionBar(binding.toolbar); - binding.toolbar.setOnClickListener((v) -> fragment.showEditTitleDialog()); + binding.toolbar.setOnClickListener((v) -> fragment.showNoteDetailsDialog()); } @Override diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java index d03374d8c..2165470d0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java @@ -17,17 +17,11 @@ public class NoteReadonlyFragment extends NotePreviewFragment { @Override public void onPrepareOptionsMenu(@NonNull Menu menu) { super.onPrepareOptionsMenu(menu); - menu.findItem(R.id.menu_favorite).setVisible(false); menu.findItem(R.id.menu_edit).setVisible(false); menu.findItem(R.id.menu_preview).setVisible(false); menu.findItem(R.id.menu_cancel).setVisible(false); menu.findItem(R.id.menu_delete).setVisible(false); - menu.findItem(R.id.menu_share).setVisible(false); menu.findItem(R.id.menu_move).setVisible(false); - menu.findItem(R.id.menu_category).setVisible(false); - menu.findItem(R.id.menu_title).setVisible(false); - if (menu.findItem(MENU_ID_PIN) != null) - menu.findItem(MENU_ID_PIN).setVisible(false); } @Nullable @@ -44,11 +38,6 @@ protected void registerInternalNoteLinkHandler() { // Do nothing } - @Override - public void showEditTitleDialog() { - // Do nothing - } - @Override public void onCloseNote() { // Do nothing 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 new file mode 100644 index 000000000..b6bf96c3d --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsDialogFragment.java @@ -0,0 +1,200 @@ +package it.niedermann.owncloud.notes.edit.details; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +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; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +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; + +import static androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported; +import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT; +import static java.lang.Boolean.TRUE; + +public class NoteDetailsDialogFragment extends DialogFragment { + + private static final String TAG = NoteDetailsDialogFragment.class.getSimpleName(); + 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; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(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 + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + binding = DialogNoteDetailsBinding.inflate(getLayoutInflater(), null, false); + + final Dialog dialog = new AlertDialog.Builder(getActivity()) + .setView(binding.getRoot()) + .setCancelable(true) + .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) -> { + 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()); + final boolean supportsTitle = preferredApiVersion != null && preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) >= 0; + 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... + } + }); + + 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); + } + }); + 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.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.favorite.setOnClickListener((v) -> viewModel.toggleFavorite(account, noteId)); + }); + return dialog; + } + + public static DialogFragment newInstance(@NonNull Account account, long noteId) { + final DialogFragment fragment = new NoteDetailsDialogFragment(); + final Bundle args = new Bundle(); + args.putSerializable(PARAM_ACCOUNT, account); + args.putLong(PARAM_NOTE_ID, 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 new file mode 100644 index 000000000..028ff0fda --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/details/NoteDetailsViewModel.java @@ -0,0 +1,65 @@ +package it.niedermann.owncloud.notes.edit.details; + +import android.app.Application; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +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 final ExecutorService executor = Executors.newCachedThreadPool(); + + @NonNull + private final NotesRepository repo; + + public NoteDetailsViewModel(@NonNull Application application) { + super(application); + this.repo = NotesRepository.getInstance(application); + } + + 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(@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 f2b242d6d..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); } @@ -450,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/drawable/ic_baseline_push_pin_24.xml b/app/src/main/res/drawable/ic_baseline_push_pin_24.xml new file mode 100644 index 000000000..1edd9e64f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_push_pin_24.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/layout/dialog_note_details.xml b/app/src/main/res/layout/dialog_note_details.xml new file mode 100644 index 000000000..5c62aea93 --- /dev/null +++ b/app/src/main/res/layout/dialog_note_details.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_note_fragment.xml b/app/src/main/res/menu/menu_note_fragment.xml index 97cfad95a..b90e2f631 100644 --- a/app/src/main/res/menu/menu_note_fragment.xml +++ b/app/src/main/res/menu/menu_note_fragment.xml @@ -9,31 +9,6 @@ android:orderInCategory="50" app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="ifRoom|collapseActionView" /> - - - - - Backup Repair We detected an irrecoverably state of the app. Please backup your unsynchronized changes and clear the storage of the Notes app. + Details + Title