diff options
55 files changed, 1589 insertions, 173 deletions
diff --git a/app/build.gradle b/app/build.gradle index 252da7a00..52e3d0a5d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "it.niedermann.nextcloud.deck" minSdkVersion 19 targetSdkVersion 29 - versionCode 1013000 - versionName "1.13.0" + versionCode 1013001 + versionName "1.13.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true javaCompileOptions { @@ -72,8 +72,8 @@ dependencies { implementation "androidx.camera:camera-view:1.0.0-alpha19" // Markdown - implementation 'com.yydcdut:markdown-processor:0.1.3' - implementation 'com.yydcdut:rxmarkdown-wrapper:0.1.3' + implementation project(path: ':markdown') + implementation fileTree(include: ['*.jar'], dir: 'libs') // Android X diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java index 3ded31bb7..1ba1415b2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java @@ -1,6 +1,7 @@ package it.niedermann.nextcloud.deck.ui; import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.sqlite.SQLiteConstraintException; @@ -251,4 +252,8 @@ public class ImportAccountActivity extends AppCompatActivity { editor.putBoolean(prefKeyWifiOnly, originalWifiOnlyValue); editor.apply(); } + + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, ImportAccountActivity.class); + } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java index d7e726a7d..06fa3186b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java @@ -132,9 +132,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener private FilterViewModel filterViewModel; private PickStackViewModel pickStackViewModel; - protected static final int ACTIVITY_ABOUT = 1; protected static final int ACTIVITY_SETTINGS = 2; - public static final int ACTIVITY_MANAGE_ACCOUNTS = 4; @NonNull protected List<Account> accountsList = new ArrayList<>(); @@ -158,8 +156,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener private boolean firstAccountAdded = false; private ConnectivityManager.NetworkCallback networkCallback; - private String accountAlreadyAdded; - private String urlFragmentUpdateDeck; private String addList; private String addBoard; @Nullable @@ -186,8 +182,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener addList = getString(R.string.add_list); addBoard = getString(R.string.add_board); - accountAlreadyAdded = getString(R.string.account_already_added); - urlFragmentUpdateDeck = getString(R.string.url_fragment_update_deck); setSupportActionBar(binding.toolbar); @@ -202,7 +196,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener if (hasAccounts) { return mainViewModel.readAccounts(); } else { - startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + startActivityForResult(ImportAccountActivity.createIntent(this), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); return null; } }).observe(this, (List<Account> accounts) -> { @@ -300,7 +294,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener Glide .with(binding.accountSwitcher.getContext()) - .load(currentAccount.getAvatarUrl(64)) + .load(currentAccount.getAvatarUrl(binding.accountSwitcher.getWidth())) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) @@ -314,7 +308,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener binding.infoBoxVersionNotSupportedText.setText(getString(R.string.info_box_version_not_supported, mainViewModel.getCurrentAccount().getServerDeckVersion(), Version.minimumSupported(this).getOriginalVersion())); binding.infoBoxVersionNotSupportedText.setOnClickListener((v) -> { Intent openURL = new Intent(Intent.ACTION_VIEW); - openURL.setData(Uri.parse(mainViewModel.getCurrentAccount().getUrl() + urlFragmentUpdateDeck)); + openURL.setData(Uri.parse(mainViewModel.getCurrentAccount().getUrl() + getString(R.string.url_fragment_update_deck))); startActivity(openURL); }); binding.infoBoxVersionNotSupported.setVisibility(View.VISIBLE); @@ -648,10 +642,10 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener public boolean onNavigationItemSelected(@NonNull MenuItem item) { switch (item.getItemId()) { case MENU_ID_ABOUT: - startActivityForResult(AboutActivity.createIntent(this, mainViewModel.getCurrentAccount()), MainActivity.ACTIVITY_ABOUT); + startActivity(AboutActivity.createIntent(this, mainViewModel.getCurrentAccount())); break; case MENU_ID_SETTINGS: - startActivityForResult(new Intent(this, SettingsActivity.class), MainActivity.ACTIVITY_SETTINGS); + startActivityForResult(SettingsActivity.createIntent(this), MainActivity.ACTIVITY_SETTINGS); break; case MENU_ID_ADD_BOARD: EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard); @@ -818,7 +812,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener .setNegativeButton(R.string.simple_discard, null) .setPositiveButton(R.string.simple_update, (dialog, whichButton) -> { final Intent openURL = new Intent(Intent.ACTION_VIEW); - openURL.setData(Uri.parse(createdAccount.getUrl() + urlFragmentUpdateDeck)); + openURL.setData(Uri.parse(createdAccount.getUrl() + getString(R.string.url_fragment_update_deck))); startActivity(openURL); finish(); }).show()); @@ -855,7 +849,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener final Throwable error = accountLiveData.getError(); if (error instanceof SQLiteConstraintException) { DeckLog.warn("Account already added"); - BrandedSnackbar.make(binding.coordinatorLayout, accountAlreadyAdded, Snackbar.LENGTH_LONG).show(); + BrandedSnackbar.make(binding.coordinatorLayout, R.string.account_already_added, Snackbar.LENGTH_LONG).show(); } else { ExceptionDialogFragment.newInstance(error, createdAccount).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java index 2339a8783..5103cf3c1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java @@ -1,6 +1,5 @@ package it.niedermann.nextcloud.deck.ui; -import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.Color; import android.os.Bundle; @@ -64,7 +63,7 @@ public abstract class PickStackActivity extends AppCompatActivity implements Bra if (hasAccounts) { return viewModel.readAccounts(); } else { - startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + startActivityForResult(ImportAccountActivity.createIntent(this), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); return null; } }).observe(this, (List<Account> accounts) -> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java index bf3734b72..d931d5c92 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java @@ -42,8 +42,6 @@ public class AboutActivity extends BrandedActivity { setSupportActionBar(binding.toolbar); binding.viewPager.setAdapter(new TabsPagerAdapter(getSupportFragmentManager(), getLifecycle(), (Account) getIntent().getSerializableExtra(BUNDLE_KEY_ACCOUNT))); new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> tab.setText(tabTitles[position])).attach(); - - setResult(RESULT_OK); } private static class TabsPagerAdapter extends FragmentStateAdapter { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java index 744498c4a..8a70cf12d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.accountswitcher; import android.app.Dialog; -import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -26,7 +25,6 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; -import static it.niedermann.nextcloud.deck.ui.MainActivity.ACTIVITY_MANAGE_ACCOUNTS; public class AccountSwitcherDialog extends BrandedDialogFragment { @@ -80,7 +78,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { }); binding.manageAccounts.setOnClickListener((v) -> { - requireActivity().startActivityForResult(new Intent(requireContext(), ManageAccountsActivity.class), ACTIVITY_MANAGE_ACCOUNTS); + requireActivity().startActivity(ManageAccountsActivity.createIntent(requireContext())); dismiss(); }); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java index 2e1ff5e06..9f170af1e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java @@ -26,6 +26,7 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ActivityEditBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity; import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; @@ -76,12 +77,13 @@ public class EditActivity extends BrandedActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); + binding = ActivityEditBinding.inflate(getLayoutInflater()); + viewModel = new ViewModelProvider(this).get(EditCardViewModel.class); + setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - viewModel = new ViewModelProvider(this).get(EditCardViewModel.class); - loadDataFromIntent(); } @@ -169,16 +171,15 @@ public class EditActivity extends BrandedActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_card_save) { - saveAndRun(super::finish); + saveAndFinish(); } return super.onOptionsItemSelected(item); } /** - * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then runs the given {@link Runnable} - * @param runnable + * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then finishes this activity. */ - private void saveAndRun(@NonNull Runnable runnable) { + private void saveAndFinish() { if (!viewModel.isPendingCreation()) { viewModel.setPendingCreation(true); final String title = viewModel.getFullCard().getCard().getTitle(); @@ -195,11 +196,13 @@ public class EditActivity extends BrandedActivity { .setOnDismissListener(dialog -> viewModel.setPendingCreation(false)) .show(); } else { - if (viewModel.isCreateMode()) { - observeOnce(viewModel.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run()); - } else { - observeOnce(viewModel.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run()); - } + final WrappedLiveData<FullCard> save$ = viewModel.saveCard(); + save$.observe(this, (fullCard) -> { + if (save$.hasError()) { + DeckLog.logError(save$.getError()); + } + }); + super.finish(); } } } @@ -272,7 +275,7 @@ public class EditActivity extends BrandedActivity { new BrandedAlertDialogBuilder(this) .setTitle(R.string.simple_save) .setMessage(R.string.do_you_want_to_save_your_changes) - .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndRun(super::finish)) + .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndFinish()) .setNegativeButton(R.string.simple_discard, (dialog, whichButton) -> super.finish()).show(); } else { super.finish(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java index c754d0800..697566f84 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java @@ -138,12 +138,13 @@ public class EditCardViewModel extends AndroidViewModel { return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId); } - public WrappedLiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) { - return syncManager.createFullCard(accountId, localBoardId, localStackId, card); - } - - public WrappedLiveData<FullCard> updateCard(@NonNull FullCard card) { - return syncManager.updateCard(card); + /** + * Saves the current {@link #fullCard}. If it is a new card, it will be created, otherwise it will be updated. + */ + public WrappedLiveData<FullCard> saveCard() { + return isCreateMode() + ? syncManager.createFullCard(getAccount().getId(), getBoardId(), getFullCard().getCard().getStackId(), getFullCard()) + : syncManager.updateCard(getFullCard()); } public LiveData<List<Activity>> syncActivitiesForCard(@NonNull Card card) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java index 34d2eb3f3..b18f35de0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java @@ -24,10 +24,13 @@ import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.ui.branding.BrandedDeleteAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; +import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialog; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -@Deprecated +/** + * TODO maybe this can be merged with {@link PreviewDialog} + */ public class CardAssigneeDialog extends BrandedDialogFragment { private static final String KEY_USER = "user"; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java index 3fd536aa6..bd6fdda43 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java @@ -34,7 +34,6 @@ import static android.view.View.VISIBLE; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB; -import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions; public class CardCommentsFragment extends BrandedFragment implements CommentEditedListener, CommentDeletedListener, CommentSelectAsReplyListener { @@ -78,9 +77,9 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit if (comment == null) { binding.replyComment.setVisibility(GONE); } else { - binding.replyCommentText.setText(comment.getComment().getMessage()); + binding.replyCommentText.setMarkdownString(comment.getComment().getMessage()); binding.replyComment.setVisibility(VISIBLE); - setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText); +// setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText); } }); commentsViewModel.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(), diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java index 3e540c95e..df44e58ef 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java @@ -14,6 +14,8 @@ import androidx.recyclerview.widget.RecyclerView; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.util.HashMap; +import java.util.Map; import it.niedermann.android.util.ClipboardUtil; import it.niedermann.android.util.DimensionUtil; @@ -21,12 +23,11 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.enums.DBStatus; +import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; import it.niedermann.nextcloud.deck.util.DateUtil; import it.niedermann.nextcloud.deck.util.ViewUtil; -import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions; - public class ItemCommentViewHolder extends RecyclerView.ViewHolder { private final ItemCommentBinding binding; private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); @@ -39,11 +40,15 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @ColorInt int mainColor, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager) { ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); - binding.message.setText(comment.getComment().getMessage()); + final Map<String, String> mentions = new HashMap<>(comment.getComment().getMentions().size()); + for (Mention mention : comment.getComment().getMentions()) { + mentions.put(mention.getMentionId(), mention.getMentionDisplayName()); + } + binding.message.setMarkdownString(comment.getComment().getMessage(), mentions); binding.actorDisplayName.setText(comment.getComment().getActorDisplayName()); binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().toEpochMilli())); - itemView.setOnClickListener(View::showContextMenu); + itemView.setOnClickListener(View::showContextMenu); itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { inflater.inflate(R.menu.comment_menu, menu); menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), comment.getComment().getMessage())); @@ -72,12 +77,10 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { } }); + TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter)); DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(comment.getStatusEnum()) ? View.VISIBLE : View.GONE); - TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter)); - setupMentions(account, comment.getComment().getMentions(), binding.message); - if (comment.getParent() == null) { binding.parentContainer.setVisibility(View.GONE); } else { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java index 2c697de08..9db572867 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java @@ -4,9 +4,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -28,8 +26,6 @@ import com.wdullaer.materialdatetimepicker.date.DatePickerDialog; import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateSetListener; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog; import com.wdullaer.materialdatetimepicker.time.TimePickerDialog.OnTimeSetListener; -import com.yydcdut.markdown.MarkdownProcessor; -import com.yydcdut.markdown.syntax.edit.EditFactory; import java.time.Instant; import java.time.LocalDate; @@ -46,6 +42,7 @@ import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabDetailsBinding; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.branding.BrandedDatePickerDialog; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; @@ -57,7 +54,6 @@ import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter; import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeDialog; import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeListener; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.util.MarkDownUtil; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -72,6 +68,7 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); private AppCompatActivity activity; + boolean editorActive = true; @Override public void onAttach(@NonNull Context context) { @@ -110,7 +107,6 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis setupDueDate(); setupDescription(); setupProjects(); - binding.description.setText(viewModel.getFullCard().getCard().getDescription()); return binding.getRoot(); } @@ -137,35 +133,41 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis applyBrandToEditText(mainColor, binding.dueDateDate); applyBrandToEditText(mainColor, binding.dueDateTime); applyBrandToEditText(mainColor, binding.people); - applyBrandToEditText(mainColor, binding.description); } private void setupDescription() { if (viewModel.canEdit()) { - MarkdownProcessor markdownProcessor = new MarkdownProcessor(requireContext()); - markdownProcessor.config(MarkDownUtil.getMarkDownConfiguration(binding.description.getContext()).build()); - markdownProcessor.factory(EditFactory.create()); - markdownProcessor.live(binding.description); - binding.description.addTextChangedListener(new TextWatcher() { - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (viewModel.getFullCard() != null) { - viewModel.getFullCard().getCard().setDescription(binding.description.getText().toString()); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // Nothing to do + binding.descriptionBar.setOnClickListener((v) -> binding.descriptionEditor.requestFocus()); + binding.descriptionToggle.setOnClickListener((v) -> { + editorActive = !editorActive; + if (editorActive) { + binding.descriptionBar.setOnClickListener((view) -> binding.descriptionEditor.requestFocus()); + binding.descriptionEditor.setVisibility(VISIBLE); + binding.descriptionViewer.setVisibility(GONE); + binding.descriptionToggle.setImageResource(R.drawable.ic_baseline_eye_24); + } else { + binding.descriptionBar.setOnClickListener(null); + binding.descriptionEditor.setVisibility(GONE); + binding.descriptionViewer.setVisibility(VISIBLE); + binding.descriptionToggle.setImageResource(R.drawable.ic_edit_grey600_24dp); } - - @Override - public void afterTextChanged(Editable s) { - // Nothing to do + }); + binding.descriptionEditor.setMarkdownString(viewModel.getFullCard().getCard().getDescription()); + binding.descriptionEditor.getMarkdownString().observe(getViewLifecycleOwner(), (newText) -> { + if (viewModel.getFullCard() != null) { + viewModel.getFullCard().getCard().setDescription(newText == null ? "" : newText.toString()); + binding.descriptionViewer.setMarkdownString(viewModel.getFullCard().getCard().getDescription()); + } else { + DeckLog.logError(new IllegalStateException(FullCard.class.getSimpleName() + " was empty when trying to setup description")); } + binding.descriptionToggle.setVisibility(TextUtils.isEmpty(newText) ? GONE : VISIBLE); }); } else { - binding.description.setEnabled(false); + binding.descriptionEditor.setEnabled(false); + binding.descriptionEditor.setVisibility(VISIBLE); + binding.descriptionViewer.setEnabled(false); + binding.descriptionViewer.setVisibility(GONE); + binding.descriptionViewer.setMarkdownString(viewModel.getFullCard().getCard().getDescription()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java index 8aa45e39a..aec258d59 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java @@ -1,8 +1,11 @@ package it.niedermann.nextcloud.deck.ui.manageaccounts; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; @@ -51,7 +54,6 @@ public class ManageAccountsActivity extends AppCompatActivity { viewModel.readAccounts().observe(this, (localAccounts -> { if (localAccounts.size() == 0) { Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName()); - setResult(AppCompatActivity.RESULT_FIRST_USER); finish(); } else { adapter.setAccounts(localAccounts); @@ -64,4 +66,8 @@ public class ManageAccountsActivity extends AppCompatActivity { public void onBackPressed() { onSupportNavigateUp(); } + + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, ManageAccountsActivity.class); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java index d65971cf4..6408f6b64 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.pickstack; import android.content.Context; -import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -142,7 +141,7 @@ public class PickStackFragment extends Fragment { if (hasAccounts) { return viewModel.readAccounts(); } else { - startActivityForResult(new Intent(requireActivity(), ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + startActivityForResult(ImportAccountActivity.createIntent(requireContext()), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); return null; } }).observe(getViewLifecycleOwner(), (List<Account> accounts) -> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java index f537c9fb4..9534b51b4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java @@ -50,7 +50,7 @@ public class AccountAdapter extends AbstractAdapter<Account> { } Glide.with(getContext()) - .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details)))) + .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size)))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java index 6f5a9a7e8..6440ba971 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java @@ -1,7 +1,10 @@ package it.niedermann.nextcloud.deck.ui.settings; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import it.niedermann.nextcloud.deck.R; @@ -11,14 +14,12 @@ import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; public class SettingsActivity extends BrandedActivity { - private ActivitySettingsBinding binding; - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this)); - binding = ActivitySettingsBinding.inflate(getLayoutInflater()); + final ActivitySettingsBinding binding = ActivitySettingsBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); @@ -40,4 +41,9 @@ public class SettingsActivity extends BrandedActivity { public void applyBrand(int mainColor) { // Nothing to do... } + + @NonNull + public static Intent createIntent(@NonNull Context context) { + return new Intent(context, SettingsActivity.class); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java index fbc7fc3fc..013a3d334 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java @@ -2,12 +2,8 @@ package it.niedermann.nextcloud.deck.util; import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.style.ImageSpan; import android.widget.ImageView; import android.widget.TextView; @@ -15,7 +11,6 @@ import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; @@ -23,16 +18,11 @@ import androidx.core.widget.TextViewCompat; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.transition.Transition; import java.time.LocalDate; -import java.util.List; import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.LOLLIPOP; @@ -88,58 +78,6 @@ public final class ViewUtil { return drawable; } - /** - * Replaces all mentions in the textView with an avatar and the display name - * - * @param account {@link Account} where the users of those mentions belong to - * @param mentions {@link List} of all mentions that should be substituted - * @param textView target {@link TextView} - */ - public static void setupMentions(@NonNull Account account, @NonNull List<Mention> mentions, TextView textView) { - Context context = textView.getContext(); - SpannableStringBuilder messageBuilder = new SpannableStringBuilder(textView.getText()); - - // Step 1 - // Add avatar icons and display names - for (Mention m : mentions) { - final String mentionId = "@" + m.getMentionId(); - final String mentionDisplayName = " " + m.getMentionDisplayName(); - int index = messageBuilder.toString().lastIndexOf(mentionId); - while (index >= 0) { - messageBuilder.setSpan(new ImageSpan(context, R.drawable.ic_person_grey600_24dp), index, index + mentionId.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - messageBuilder.insert(index + mentionId.length(), mentionDisplayName); - index = messageBuilder.toString().substring(0, index).lastIndexOf(mentionId); - } - } - textView.setText(messageBuilder); - - // Step 2 - // Replace avatar icons with real avatars - final ImageSpan[] list = messageBuilder.getSpans(0, messageBuilder.length(), ImageSpan.class); - for (ImageSpan span : list) { - final int spanStart = messageBuilder.getSpanStart(span); - final int spanEnd = messageBuilder.getSpanEnd(span); - Glide.with(context) - .asBitmap() - .placeholder(R.drawable.ic_person_grey600_24dp) - .load(account.getUrl() + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + DimensionUtil.INSTANCE.dpToPx(context, R.dimen.icon_size_details)) - .apply(RequestOptions.circleCropTransform()) - .into(new CustomTarget<Bitmap>() { - @Override - public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { - messageBuilder.removeSpan(span); - messageBuilder.setSpan(new ImageSpan(context, resource), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - // silence is gold - } - }); - } - textView.setText(messageBuilder); - } - public static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @ColorRes int colorRes) { if (SDK_INT >= LOLLIPOP) { imageView.setImageTintList(ColorStateList.valueOf(ContextCompat.getColor(context, colorRes))); diff --git a/app/src/main/res/drawable/ic_baseline_eye_24.xml b/app/src/main/res/drawable/ic_baseline_eye_24.xml new file mode 100644 index 000000000..c8acf29a8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_eye_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#757575" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/> +</vector> diff --git a/app/src/main/res/layout/fragment_card_edit_tab_comments.xml b/app/src/main/res/layout/fragment_card_edit_tab_comments.xml index cc5fa77b7..4e340c3a2 100644 --- a/app/src/main/res/layout/fragment_card_edit_tab_comments.xml +++ b/app/src/main/res/layout/fragment_card_edit_tab_comments.xml @@ -49,7 +49,7 @@ android:padding="@dimen/spacer_1x" app:srcCompat="@drawable/ic_reply_grey600_24dp" /> - <TextView + <it.niedermann.android.markdown.MarkdownViewerImpl android:id="@+id/replyCommentText" android:layout_width="0dp" android:layout_height="wrap_content" diff --git a/app/src/main/res/layout/fragment_card_edit_tab_details.xml b/app/src/main/res/layout/fragment_card_edit_tab_details.xml index 2dfb79889..a2f5e5b21 100644 --- a/app/src/main/res/layout/fragment_card_edit_tab_details.xml +++ b/app/src/main/res/layout/fragment_card_edit_tab_details.xml @@ -131,29 +131,56 @@ tools:listitem="@tools:sample/avatars" /> <LinearLayout + android:id="@+id/descriptionBar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacer_2x" android:orientation="horizontal"> <ImageView android:layout_width="@dimen/icon_size_details" android:layout_height="wrap_content" - android:layout_marginTop="10dp" android:layout_marginEnd="@dimen/spacer_2x" android:contentDescription="@null" app:srcCompat="@drawable/ic_baseline_subject_24" /> - <com.yydcdut.markdown.MarkdownEditText - android:id="@+id/description" - android:layout_width="match_parent" + <LinearLayout + android:layout_width="0dp" android:layout_height="wrap_content" - android:gravity="top" - android:hint="@string/label_description" - android:importantForAutofill="no" - android:inputType="textMultiLine|textCapSentences" - android:scrollbars="vertical" /> + android:layout_weight="1" + android:gravity="end" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/descriptionToggle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/edit_description" + android:paddingStart="@dimen/spacer_1x" + android:paddingEnd="@dimen/spacer_1x" + android:visibility="gone" + app:srcCompat="@drawable/ic_baseline_eye_24" + tools:visibility="gone" /> + </LinearLayout> </LinearLayout> + + <it.niedermann.android.markdown.MarkdownEditorImpl + android:id="@+id/descriptionEditor" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacer_1x" + android:textColor="?attr/colorAccent" + android:textSize="@dimen/font_size_description" /> + + <it.niedermann.android.markdown.MarkdownViewerImpl + android:id="@+id/descriptionViewer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacer_2x" + android:textColor="?attr/colorAccent" + android:textIsSelectable="true" + android:textSize="@dimen/font_size_description" + android:visibility="gone" /> </LinearLayout> <TextView diff --git a/app/src/main/res/layout/item_comment.xml b/app/src/main/res/layout/item_comment.xml index 754e989fe..2863699a5 100644 --- a/app/src/main/res/layout/item_comment.xml +++ b/app/src/main/res/layout/item_comment.xml @@ -94,7 +94,7 @@ tools:text="@tools:sample/date/day_of_week" /> </LinearLayout> - <com.yydcdut.markdown.MarkdownTextView + <it.niedermann.android.markdown.MarkdownViewerImpl android:id="@+id/message" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index edd334cf3..e44e774b3 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -109,6 +109,7 @@ <string name="no_lists_yet">Još nema popisa</string> <string name="do_you_want_to_save_your_changes">Želite li spremiti promjene?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Želite li arhivirati sve kartice iz %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Želite li arhivirati sve filtrirane kartice od %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Trajno ćete izbrisati %1$d karticu s ovog popisa.</item> <item quantity="few">Trajno ćete izbrisati %1$d kartice s ovog popisa.</item> @@ -278,4 +279,8 @@ <string name="project_type_room">Soba za razgovor</string> <string name="simple_move">Premjesti</string> <string name="cannot_upload_files_without_permission">Otpremanje datoteka bez dopuštenja nije moguće</string> + <string name="clone_cards">Kloniraj kartice</string> + <string name="simple_clone">Kloniraj</string> + <string name="user_avatar">Avatar korisnika</string> + <string name="simple_unassign">Poništi dodjelu</string> </resources> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index c19fe8de1..ac7a62c15 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -9,6 +9,8 @@ <dimen name="compact_label_height">6dp</dimen> <dimen name="attachments_bottom_navigation_height">64dp</dimen> + <dimen name="font_size_description">18sp</dimen> + <!-- Drawer header --> <dimen name="drawer_header_height">100dp</dimen> <dimen name="drawer_header_logo_size">42dp</dimen> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b152a2a6f..5c7aaf716 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -323,4 +323,5 @@ <string name="add_stack_widget">Add list widget</string> <string name="add_filter_widget">Add filter widget</string> <string name="simple_order">Order</string> + <string name="edit_description">Edit description</string> </resources> diff --git a/fastlane/metadata/android/en-US/changelogs/1013001.txt b/fastlane/metadata/android/en-US/changelogs/1013001.txt new file mode 100644 index 000000000..daa156ce1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1013001.txt @@ -0,0 +1,3 @@ +- 📝 Enhanced Markdown editor for description (#452) +- 📝 Markdown viewer for description with support for tables and syntax highlighting +- 📝 Markdown support for comments
\ No newline at end of file diff --git a/markdown/.gitignore b/markdown/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/markdown/.gitignore @@ -0,0 +1 @@ +/build
\ No newline at end of file diff --git a/markdown/build.gradle b/markdown/build.gradle new file mode 100644 index 000000000..f7f7c6c09 --- /dev/null +++ b/markdown/build.gradle @@ -0,0 +1,68 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.1" + + defaultConfig { + minSdkVersion 19 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + multiDexEnabled true + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + coreLibraryDesugaringEnabled true + } +} + +dependencies { + implementation 'com.github.nextcloud:Android-SingleSignOn:0.5.4' + + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation "androidx.lifecycle:lifecycle-livedata:2.2.0" + + implementation "io.noties.markwon:core:4.6.0" + implementation "io.noties.markwon:editor:4.6.0" + implementation "io.noties.markwon:ext-strikethrough:4.6.0" + implementation "io.noties.markwon:ext-tables:4.6.0" + implementation "io.noties.markwon:ext-tasklist:4.6.0" + implementation "io.noties.markwon:html:4.6.0" + implementation "io.noties.markwon:image:4.6.0" + implementation "io.noties.markwon:image-glide:4.6.0" + implementation "io.noties.markwon:linkify:4.6.0" + implementation "io.noties.markwon:simple-ext:4.6.0" + implementation "io.noties.markwon:inline-parser:4.6.0" + implementation("io.noties.markwon:syntax-highlight:4.6.0") { + exclude group: 'org.jetbrains', module: 'annotations-java5' + } + implementation("io.noties:prism4j:2.0.0") { + exclude group: 'org.jetbrains', module: 'annotations-java5' + } + annotationProcessor "io.noties:prism4j-bundler:2.0.0" + implementation 'org.jetbrains:annotations:15.0' + + implementation 'com.yydcdut:markdown-processor:0.1.3' + implementation 'com.yydcdut:rxmarkdown-wrapper:0.1.3' + + implementation 'androidx.multidex:multidex:2.0.1' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + + testImplementation 'junit:junit:4.13.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' +}
\ No newline at end of file diff --git a/markdown/consumer-rules.pro b/markdown/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/markdown/consumer-rules.pro diff --git a/markdown/proguard-rules.pro b/markdown/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/markdown/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile
\ No newline at end of file diff --git a/markdown/src/androidTest/java/it/niedermann/android/markdown/ExampleInstrumentedTest.java b/markdown/src/androidTest/java/it/niedermann/android/markdown/ExampleInstrumentedTest.java new file mode 100644 index 000000000..e765e0663 --- /dev/null +++ b/markdown/src/androidTest/java/it/niedermann/android/markdown/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package it.niedermann.android.markdown; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("it.niedermann.android.markdown.test", appContext.getPackageName()); + } +}
\ No newline at end of file diff --git a/markdown/src/main/AndroidManifest.xml b/markdown/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b4ee796a7 --- /dev/null +++ b/markdown/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest package="it.niedermann.android.markdown"> + +</manifest>
\ No newline at end of file diff --git a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditor.java b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditor.java new file mode 100644 index 000000000..6b08cfd82 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditor.java @@ -0,0 +1,34 @@ +package it.niedermann.android.markdown; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; + +import java.util.Map; + +/** + * Can be used for editors and viewers as well. + * Viewer can support basic edit features, like toggling checkboxes + */ +public interface MarkdownEditor { + + /** + * The given {@link String} will be parsed and rendered + */ + void setMarkdownString(CharSequence text); + + /** + * Will replace all `@mention`s of Nextcloud users with the avatar and given display name. + * + * @param mentions {@link Map} of mentions, where the key is the user id and the value is the display name + */ + default void setMarkdownString(CharSequence text, @NonNull Map<String, String> mentions) { + setMarkdownString(text); + } + + /** + * @return the source {@link String} of the currently rendered markdown + */ + LiveData<CharSequence> getMarkdownString(); + + void setEnabled(boolean enabled); +}
\ No newline at end of file diff --git a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditorImpl.java b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditorImpl.java new file mode 100644 index 000000000..6e4e10484 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditorImpl.java @@ -0,0 +1,23 @@ +package it.niedermann.android.markdown; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor; + +public class MarkdownEditorImpl extends MarkwonMarkdownEditor { + public MarkdownEditorImpl(@NonNull Context context) { + super(context); + } + + public MarkdownEditorImpl(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public MarkdownEditorImpl(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/MarkdownViewerImpl.java b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownViewerImpl.java new file mode 100644 index 000000000..ae67d93d1 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/MarkdownViewerImpl.java @@ -0,0 +1,24 @@ +package it.niedermann.android.markdown; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import it.niedermann.android.markdown.markwon.MarkwonMarkdownViewer; + +public class MarkdownViewerImpl extends MarkwonMarkdownViewer { + + public MarkdownViewerImpl(@NonNull Context context) { + super(context); + } + + public MarkdownViewerImpl(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public MarkdownViewerImpl(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/MentionUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/MentionUtil.java new file mode 100644 index 000000000..e0639fd89 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/MentionUtil.java @@ -0,0 +1,95 @@ +package it.niedermann.android.markdown; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; +import com.nextcloud.android.sso.model.SingleSignOnAccount; + +import java.util.Map; + +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class MentionUtil { + + private MentionUtil() { + // Util class + } + + /** + * Replaces all mentions in the textView with an avatar and the display name + * + * @param account {@link SingleSignOnAccount} where the users of those mentions belong to + * @param mentions {@link Map} of all mentions that should be substituted, the key is the user id and the value the display name + * @param target target {@link TextView} + */ + public static void setupMentions(@NonNull SingleSignOnAccount account, @NonNull Map<String, String> mentions, @NonNull TextView target) { + final Context context = target.getContext(); + + // Step 1 + // Add avatar icons and display names + final SpannableStringBuilder messageBuilder = replaceAtMentionsWithImagePlaceholderAndDisplayName(context, mentions, target.getText()); + + // Step 2 + // Replace avatar icons with real avatars + final MentionSpan[] list = messageBuilder.getSpans(0, messageBuilder.length(), MentionSpan.class); + for (MentionSpan span : list) { + final int spanStart = messageBuilder.getSpanStart(span); + final int spanEnd = messageBuilder.getSpanEnd(span); + Glide.with(context) + .asBitmap() + .placeholder(R.drawable.ic_person_grey600_24dp) + .load(account.url + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + span.getDrawable().getIntrinsicHeight()) + .apply(RequestOptions.circleCropTransform()) + .into(new CustomTarget<Bitmap>() { + @Override + public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) { + messageBuilder.removeSpan(span); + messageBuilder.setSpan(new MentionSpan(context, resource), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + // silence is gold + } + }); + } + target.setText(messageBuilder); + } + + private static SpannableStringBuilder replaceAtMentionsWithImagePlaceholderAndDisplayName(@NonNull Context context, @NonNull Map<String, String> mentions, @NonNull CharSequence text) { + final SpannableStringBuilder messageBuilder = new SpannableStringBuilder(text); + for (String userId : mentions.keySet()) { + final String mentionId = "@" + userId; + final String mentionDisplayName = " " + mentions.get(userId); + int index = messageBuilder.toString().lastIndexOf(mentionId); + while (index >= 0) { + messageBuilder.setSpan(new MentionSpan(context, R.drawable.ic_person_grey600_24dp), index, index + mentionId.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + messageBuilder.insert(index + mentionId.length(), mentionDisplayName); + index = messageBuilder.toString().substring(0, index).lastIndexOf(mentionId); + } + } + return messageBuilder; + } + + private static class MentionSpan extends ImageSpan { + private MentionSpan(@NonNull Context context, int resourceId) { + super(context, resourceId); + } + + private MentionSpan(@NonNull Context context, @NonNull Bitmap bitmap) { + super(context, bitmap); + } + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java new file mode 100644 index 000000000..7dd73a94b --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java @@ -0,0 +1,70 @@ +package it.niedermann.android.markdown.markwon; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import io.noties.markwon.Markwon; +import io.noties.markwon.editor.MarkwonEditor; +import io.noties.markwon.editor.handler.EmphasisEditHandler; +import io.noties.markwon.editor.handler.StrongEmphasisEditHandler; +import it.niedermann.android.markdown.MarkdownEditor; +import it.niedermann.android.markdown.markwon.handler.BlockQuoteEditHandler; +import it.niedermann.android.markdown.markwon.handler.CodeBlockEditHandler; +import it.niedermann.android.markdown.markwon.handler.CodeEditHandler; +import it.niedermann.android.markdown.markwon.handler.HeadingEditHandler; +import it.niedermann.android.markdown.markwon.handler.StrikethroughEditHandler; +import it.niedermann.android.markdown.markwon.textwatcher.AutoContinuationTextWatcher; + +public class MarkwonMarkdownEditor extends AppCompatEditText implements MarkdownEditor { + + private final MutableLiveData<CharSequence> unrenderedText$ = new MutableLiveData<>(); + + public MarkwonMarkdownEditor(@NonNull Context context) { + this(context, null); + } + + public MarkwonMarkdownEditor(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, android.R.attr.editTextStyle); + } + + public MarkwonMarkdownEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + final Markwon markwon = MarkwonMarkdownUtil.initMarkwonEditor(context).build(); + final MarkwonEditor editor = MarkwonEditor.builder(markwon) + .useEditHandler(new EmphasisEditHandler()) + .useEditHandler(new StrongEmphasisEditHandler()) + .useEditHandler(new StrikethroughEditHandler()) + .useEditHandler(new CodeEditHandler()) + .useEditHandler(new CodeBlockEditHandler()) + .useEditHandler(new BlockQuoteEditHandler()) + .useEditHandler(new HeadingEditHandler()) + .build(); + addTextChangedListener(new AutoContinuationTextWatcher(editor, this)); + } + + @Override + public void setMarkdownString(CharSequence text) { + setText(text); + setMarkdownStringModel(text); + } + + /** + * Updates the current model which matches the rendered state of the editor *without* triggering + * anything of the native {@link EditText} + */ + public void setMarkdownStringModel(CharSequence text) { + unrenderedText$.setValue(text == null ? "" : text.toString()); + } + + @Override + public LiveData<CharSequence> getMarkdownString() { + return unrenderedText$; + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java new file mode 100644 index 000000000..d33401b47 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java @@ -0,0 +1,65 @@ +package it.niedermann.android.markdown.markwon; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +import java.util.Map; + +import io.noties.markwon.Markwon; +import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; +import io.noties.markwon.ext.tables.TablePlugin; +import io.noties.markwon.ext.tasklist.TaskListPlugin; +import io.noties.markwon.image.ImagesPlugin; +import io.noties.markwon.image.glide.GlideImagesPlugin; +import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; +import io.noties.markwon.linkify.LinkifyPlugin; +import io.noties.markwon.simple.ext.SimpleExtPlugin; +import io.noties.markwon.syntax.Prism4jTheme; +import io.noties.markwon.syntax.Prism4jThemeDefault; +import io.noties.markwon.syntax.SyntaxHighlightPlugin; +import io.noties.prism4j.Prism4j; +import io.noties.prism4j.annotations.PrismBundle; +import it.niedermann.android.markdown.markwon.plugins.NextcloudMentionsPlugin; +import it.niedermann.android.markdown.markwon.plugins.ThemePlugin; + +@RestrictTo(value = RestrictTo.Scope.LIBRARY) +@PrismBundle( + include = { + "c", "clike", "clojure", "cpp", "csharp", "css", "dart", "git", "go", "groovy", "java", "javascript", "json", + "kotlin", "latex", "makefile", "markdown", "markup", "python", "scala", "sql", "swift", "yaml" + }, + grammarLocatorClassName = ".MarkwonGrammarLocator" +) +public class MarkwonMarkdownUtil { + + private MarkwonMarkdownUtil() { + // Util class + } + + public static Markwon.Builder initMarkwonEditor(@NonNull Context context) { + return Markwon.builder(context) + .usePlugin(ThemePlugin.create(context)) + .usePlugin(StrikethroughPlugin.create()) + .usePlugin(SimpleExtPlugin.create()) + .usePlugin(ImagesPlugin.create()) + .usePlugin(MarkwonInlineParserPlugin.create()); + } + + public static Markwon.Builder initMarkwonViewer(@NonNull Context context) { + final Prism4j prism4j = new Prism4j(new MarkwonGrammarLocator()); + final Prism4jTheme prism4jTheme = Prism4jThemeDefault.create(); + return initMarkwonEditor(context) + .usePlugin(TablePlugin.create(context)) + .usePlugin(TaskListPlugin.create(context)) + .usePlugin(LinkifyPlugin.create()) + .usePlugin(GlideImagesPlugin.create(context)) + .usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme)); + } + + public static Markwon.Builder initMarkwonViewer(@NonNull Context context, @NonNull Map<String, String> mentions) { + return initMarkwonViewer(context) + .usePlugin(NextcloudMentionsPlugin.create(context, mentions)); + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java new file mode 100644 index 000000000..aa6bcc8f9 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java @@ -0,0 +1,72 @@ +package it.niedermann.android.markdown.markwon; + +import android.content.Context; +import android.text.Spanned; +import android.text.TextUtils; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import io.noties.markwon.Markwon; +import it.niedermann.android.markdown.MarkdownEditor; + +import static androidx.lifecycle.Transformations.distinctUntilChanged; +import static it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil.initMarkwonViewer; + +public class MarkwonMarkdownViewer extends AppCompatTextView implements MarkdownEditor { + + private Markwon markwon; + private final MutableLiveData<CharSequence> unrenderedText$ = new MutableLiveData<>(); + private final ExecutorService renderService; + + public MarkwonMarkdownViewer(@NonNull Context context) { + this(context, null); + } + + public MarkwonMarkdownViewer(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + public MarkwonMarkdownViewer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.markwon = MarkwonMarkdownUtil.initMarkwonViewer(context).build(); + this.renderService = Executors.newSingleThreadExecutor(); + } + + @Override + public void setMarkdownString(CharSequence text) { + final CharSequence previousText = this.unrenderedText$.getValue(); + this.unrenderedText$.setValue(text); + if (TextUtils.isEmpty(text)) { + setText(text); + } else { + if (!text.equals(previousText)) { + setText(text); + this.renderService.execute(() -> { + final Spanned markdown = this.markwon.toMarkdown(text.toString()); + post(() -> this.markwon.setParsedMarkdown(this, markdown)); + }); + } + + } + } + + @Override + public void setMarkdownString(CharSequence text, @NonNull Map<String, String> mentions) { + this.markwon = initMarkwonViewer(getContext(), mentions).build(); + setMarkdownString(text); + } + + @Override + public LiveData<CharSequence> getMarkdownString() { + return distinctUntilChanged(this.unrenderedText$); + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/BlockQuoteEditHandler.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/BlockQuoteEditHandler.java new file mode 100644 index 000000000..2f81dc79b --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/BlockQuoteEditHandler.java @@ -0,0 +1,50 @@ +package it.niedermann.android.markdown.markwon.handler; + +import android.text.Editable; +import android.text.Spanned; + +import androidx.annotation.NonNull; + +import io.noties.markwon.Markwon; +import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.core.spans.BlockQuoteSpan; +import io.noties.markwon.editor.EditHandler; +import io.noties.markwon.editor.PersistedSpans; + +public class BlockQuoteEditHandler implements EditHandler<BlockQuoteSpan> { + + private MarkwonTheme theme; + + @Override + public void init(@NonNull Markwon markwon) { + this.theme = markwon.configuration().theme(); + } + + @Override + public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { + builder.persistSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme)); + } + + @Override + public void handleMarkdownSpan( + @NonNull PersistedSpans persistedSpans, + @NonNull Editable editable, + @NonNull String input, + @NonNull BlockQuoteSpan span, + int spanStart, + int spanTextLength) { + // todo: here we should actually find a proper ending of a block quote... + editable.setSpan( + persistedSpans.get(BlockQuoteSpan.class), + spanStart, + spanStart + spanTextLength, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + + @NonNull + @Override + public Class<BlockQuoteSpan> markdownSpanType() { + return BlockQuoteSpan.class; + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeBlockEditHandler.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeBlockEditHandler.java new file mode 100644 index 000000000..0f94bba41 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeBlockEditHandler.java @@ -0,0 +1,47 @@ +package it.niedermann.android.markdown.markwon.handler; + +import android.text.Editable; +import android.text.Spanned; + +import androidx.annotation.NonNull; + +import io.noties.markwon.Markwon; +import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.core.spans.CodeBlockSpan; +import io.noties.markwon.editor.EditHandler; +import io.noties.markwon.editor.MarkwonEditorUtils; +import io.noties.markwon.editor.PersistedSpans; + +public class CodeBlockEditHandler implements EditHandler<CodeBlockSpan> { + private MarkwonTheme theme; + + @Override + public void init(@NonNull Markwon markwon) { + theme = markwon.configuration().theme(); + } + + @Override + public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { + builder.persistSpan(CodeBlockSpan.class, () -> new CodeBlockSpan(theme)); + } + + @Override + public void handleMarkdownSpan(@NonNull PersistedSpans persistedSpans, @NonNull Editable editable, @NonNull String input, @NonNull CodeBlockSpan span, int spanStart, int spanTextLength) { + MarkwonEditorUtils.Match delimited = MarkwonEditorUtils.findDelimited(input, spanStart, "```"); + if (delimited != null) { + editable.setSpan( + persistedSpans.get(markdownSpanType()), + delimited.start(), + delimited.end(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + + ); + } + } + + @NonNull + @Override + public Class<CodeBlockSpan> markdownSpanType() { + return CodeBlockSpan.class; + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeEditHandler.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeEditHandler.java new file mode 100644 index 000000000..e4893ce3e --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeEditHandler.java @@ -0,0 +1,54 @@ +package it.niedermann.android.markdown.markwon.handler; + +import android.text.Editable; +import android.text.Spanned; + +import androidx.annotation.NonNull; + +import io.noties.markwon.Markwon; +import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.core.spans.CodeSpan; +import io.noties.markwon.editor.EditHandler; +import io.noties.markwon.editor.MarkwonEditorUtils; +import io.noties.markwon.editor.PersistedSpans; + +public class CodeEditHandler implements EditHandler<CodeSpan> { + + private MarkwonTheme theme; + + @Override + public void init(@NonNull Markwon markwon) { + this.theme = markwon.configuration().theme(); + } + + @Override + public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { + builder.persistSpan(CodeSpan.class, () -> new CodeSpan(theme)); + } + + @Override + public void handleMarkdownSpan( + @NonNull PersistedSpans persistedSpans, + @NonNull Editable editable, + @NonNull String input, + @NonNull CodeSpan span, + int spanStart, + int spanTextLength) { + final MarkwonEditorUtils.Match match = + MarkwonEditorUtils.findDelimited(input, spanStart, "`"); + if (match != null) { + editable.setSpan( + persistedSpans.get(CodeSpan.class), + match.start(), + match.end(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + } + + @NonNull + @Override + public Class<CodeSpan> markdownSpanType() { + return CodeSpan.class; + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/HeadingEditHandler.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/HeadingEditHandler.java new file mode 100644 index 000000000..491ce505b --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/HeadingEditHandler.java @@ -0,0 +1,142 @@ +package it.niedermann.android.markdown.markwon.handler; + +import android.text.Editable; +import android.text.Spanned; + +import androidx.annotation.NonNull; + +import io.noties.markwon.Markwon; +import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.core.spans.HeadingSpan; +import io.noties.markwon.editor.EditHandler; +import io.noties.markwon.editor.PersistedSpans; + +public class HeadingEditHandler implements EditHandler<HeadingSpan> { + + private MarkwonTheme theme; + + @Override + public void init(@NonNull Markwon markwon) { + this.theme = markwon.configuration().theme(); + } + + @Override + public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { + builder.persistSpan(Heading1Span.class, () -> new Heading1Span(theme)); + builder.persistSpan(Heading2Span.class, () -> new Heading2Span(theme)); + builder.persistSpan(Heading3Span.class, () -> new Heading3Span(theme)); + builder.persistSpan(Heading4Span.class, () -> new Heading4Span(theme)); + builder.persistSpan(Heading5Span.class, () -> new Heading5Span(theme)); + builder.persistSpan(Heading6Span.class, () -> new Heading6Span(theme)); + } + + @Override + public void handleMarkdownSpan( + @NonNull PersistedSpans persistedSpans, + @NonNull Editable editable, + @NonNull String input, + @NonNull HeadingSpan span, + int spanStart, + int spanTextLength) { + final HeadingSpan newSpan; + + switch (span.getLevel()) { + case 1: + newSpan = persistedSpans.get(Heading1Span.class); + break; + case 2: + newSpan = persistedSpans.get(Heading2Span.class); + break; + case 3: + newSpan = persistedSpans.get(Heading3Span.class); + break; + case 4: + newSpan = persistedSpans.get(Heading4Span.class); + break; + case 5: + newSpan = persistedSpans.get(Heading5Span.class); + break; + case 6: + newSpan = persistedSpans.get(Heading6Span.class); + break; + default: + return; + + } + final int newStart = getNewSpanStart(input, spanStart); + final int newEnd = findEnd(input, newStart, newSpan.getLevel()); + + editable.setSpan( + newSpan, + newStart, + newEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + + private int findEnd(String input, int searchFrom, int spanLevel) { + int end = searchFrom + spanLevel; + final int strLength = input.length(); + while (end < strLength - 1) { + end++; + if (input.charAt(end) == '\n') { + break; + } + } + return end + 1; + } + + private int getNewSpanStart(String input, int spanStart) { + int start = spanStart; + + while (start >= 0 && input.charAt(start) != '\n') { + start--; + } + start += 1; + + return start; + } + + + @NonNull + @Override + public Class<HeadingSpan> markdownSpanType() { + return HeadingSpan.class; + } + + private static class Heading1Span extends HeadingSpan { + public Heading1Span(@NonNull MarkwonTheme theme) { + super(theme, 1); + } + } + + private static class Heading2Span extends HeadingSpan { + public Heading2Span(@NonNull MarkwonTheme theme) { + super(theme, 2); + } + } + + private static class Heading3Span extends HeadingSpan { + public Heading3Span(@NonNull MarkwonTheme theme) { + super(theme, 3); + } + } + + private static class Heading4Span extends HeadingSpan { + public Heading4Span(@NonNull MarkwonTheme theme) { + super(theme, 4); + } + } + + private static class Heading5Span extends HeadingSpan { + public Heading5Span(@NonNull MarkwonTheme theme) { + super(theme, 5); + } + } + + private static class Heading6Span extends HeadingSpan { + public Heading6Span(@NonNull MarkwonTheme theme) { + super(theme, 6); + } + } +}
\ No newline at end of file diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/LinkEditHandler.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/LinkEditHandler.java new file mode 100644 index 000000000..821882f23 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/LinkEditHandler.java @@ -0,0 +1,86 @@ +package it.niedermann.android.markdown.markwon.handler; + +import android.text.Editable; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.view.View; + +import androidx.annotation.NonNull; + +import io.noties.markwon.core.spans.LinkSpan; +import io.noties.markwon.editor.AbstractEditHandler; +import io.noties.markwon.editor.PersistedSpans; + +public class LinkEditHandler extends AbstractEditHandler<LinkSpan> { + + public interface OnClick { + void onClick(@NonNull View widget, @NonNull String link); + } + + private final OnClick onClick; + + public LinkEditHandler(@NonNull OnClick onClick) { + this.onClick = onClick; + } + + @Override + public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { + builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick)); + } + + @Override + public void handleMarkdownSpan( + @NonNull PersistedSpans persistedSpans, + @NonNull Editable editable, + @NonNull String input, + @NonNull LinkSpan span, + int spanStart, + int spanTextLength) { + + final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class); + editLinkSpan.link = span.getLink(); + + final int s; + final int e; + + // markdown link vs. autolink + if ('[' == input.charAt(spanStart)) { + s = spanStart + 1; + e = spanStart + 1 + spanTextLength; + } else { + s = spanStart; + e = spanStart + spanTextLength; + } + + editable.setSpan( + editLinkSpan, + s, + e, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + + @NonNull + @Override + public Class<LinkSpan> markdownSpanType() { + return LinkSpan.class; + } + + static class EditLinkSpan extends ClickableSpan { + + private final OnClick onClick; + + String link; + + EditLinkSpan(@NonNull OnClick onClick) { + this.onClick = onClick; + } + + @Override + public void onClick(@NonNull View widget) { + if (link != null) { + onClick.onClick(widget, link); + } + } + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/StrikethroughEditHandler.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/StrikethroughEditHandler.java new file mode 100644 index 000000000..d7a361dc5 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/StrikethroughEditHandler.java @@ -0,0 +1,45 @@ +package it.niedermann.android.markdown.markwon.handler; + +import android.text.Editable; +import android.text.Spanned; +import android.text.style.StrikethroughSpan; + +import androidx.annotation.NonNull; + +import io.noties.markwon.editor.AbstractEditHandler; +import io.noties.markwon.editor.MarkwonEditorUtils; +import io.noties.markwon.editor.PersistedSpans; + +public class StrikethroughEditHandler extends AbstractEditHandler<StrikethroughSpan> { + + @Override + public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { + builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new); + } + + @Override + public void handleMarkdownSpan( + @NonNull PersistedSpans persistedSpans, + @NonNull Editable editable, + @NonNull String input, + @NonNull StrikethroughSpan span, + int spanStart, + int spanTextLength) { + final MarkwonEditorUtils.Match match = + MarkwonEditorUtils.findDelimited(input, spanStart, "~~"); + if (match != null) { + editable.setSpan( + persistedSpans.get(StrikethroughSpan.class), + match.start(), + match.end(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + } + + @NonNull + @Override + public Class<StrikethroughSpan> markdownSpanType() { + return StrikethroughSpan.class; + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/NextcloudMentionsPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/NextcloudMentionsPlugin.java new file mode 100644 index 000000000..5617d5b1f --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/NextcloudMentionsPlugin.java @@ -0,0 +1,44 @@ +package it.niedermann.android.markdown.markwon.plugins; + +import android.content.Context; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; + +import java.util.Map; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonPlugin; + +import static it.niedermann.android.markdown.MentionUtil.setupMentions; + +public class NextcloudMentionsPlugin extends AbstractMarkwonPlugin { + + @NonNull + private final Context context; + @NonNull + private final Map<String, String> mentions; + + private NextcloudMentionsPlugin(@NonNull Context context, @NonNull Map<String, String> mentions) { + this.context = context.getApplicationContext(); + this.mentions = mentions; + } + + public static MarkwonPlugin create(@NonNull Context context, @NonNull Map<String, String> mentions) { + return new NextcloudMentionsPlugin(context, mentions); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + super.afterSetText(textView); + try { + setupMentions(SingleAccountHelper.getCurrentSingleSignOnAccount(context), mentions, textView); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + e.printStackTrace(); + } + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ThemePlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ThemePlugin.java new file mode 100644 index 000000000..50bcb7fcf --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ThemePlugin.java @@ -0,0 +1,32 @@ +package it.niedermann.android.markdown.markwon.plugins; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonPlugin; +import io.noties.markwon.core.MarkwonTheme; +import it.niedermann.android.markdown.R; + +public class ThemePlugin extends AbstractMarkwonPlugin { + + @NonNull Context context; + + private ThemePlugin(@NonNull Context context) { + this.context = context; + } + + public static MarkwonPlugin create(@NonNull Context context) { + return new ThemePlugin(context); + } + + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + super.configureTheme(builder); + builder + .bulletWidth(context.getResources().getDimensionPixelSize(R.dimen.bullet_point_width)) + .headingBreakHeight(0) + .headingTextSizeMultipliers(new float[]{1.45f, 1.35f, 1.25f, 1.15f, 1.1f, 1.05f}); + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java new file mode 100644 index 000000000..cf88f35bc --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java @@ -0,0 +1,109 @@ +package it.niedermann.android.markdown.markwon.plugins; + +import android.text.Spanned; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonSpansFactory; +import io.noties.markwon.SpanFactory; +import io.noties.markwon.ext.tasklist.TaskListItem; +import io.noties.markwon.ext.tasklist.TaskListSpan; + +public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin { + + private final String originalNoteContent; + private final ToggleListener toggleListener; + public static final String CHECKBOX_UNCHECKED_PLUS = "+ [ ]"; + public static final String CHECKBOX_UNCHECKED_MINUS = "- [ ]"; + public static final String CHECKBOX_UNCHECKED_STAR = "* [ ]"; + public static final String CHECKBOX_CHECKED_PLUS = "+ [x]"; + public static final String CHECKBOX_CHECKED_MINUS = "- [x]"; + public static final String CHECKBOX_CHECKED_STAR = "* [x]"; + + public ToggleableTaskListPlugin(String originalNoteContent, ToggleListener toggleListener) { + this.originalNoteContent = originalNoteContent; + this.toggleListener = toggleListener; + } + + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + SpanFactory origin = builder.getFactory(TaskListItem.class); + + builder.setFactory(TaskListItem.class, (configuration, props) -> { + TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props); + ClickableSpan c = new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + Log.v("checkbox", "abcdef"); + span.setDone(!span.isDone()); + widget.invalidate(); + + // it must be a TextView + final TextView textView = (TextView) widget; + // it must be spanned + final Spanned spanned = (Spanned) textView.getText(); + + // actual text of the span (this can be used along with the `span`) + final CharSequence task = spanned.subSequence( + spanned.getSpanStart(this), + spanned.getSpanEnd(this) + ); + + int lineNumber = 0; + + CharSequence textBeforeTask = spanned.subSequence(0, spanned.getSpanStart(this)); + for (int i = 0; i < textBeforeTask.length(); i++) { + if (textBeforeTask.charAt(i) == '\n') + lineNumber++; + } + + // Work on the original content now, because the previous stuff is rendered and inline markdown might be removed at this point + + String[] lines = TextUtils.split(originalNoteContent, "\\r?\\n"); + /* + * When (un)checking a checkbox in a note which contains code-blocks, the "`"-characters get stripped out in the TextView and therefore the given lineNumber is wrong + * Find number of lines starting with ``` before lineNumber + */ + // TODO Maybe one can simpliy write i < lineNumber? + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("```")) { + lineNumber++; + } + if (i == lineNumber) { + break; + } + } + + if (lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_MINUS) || lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_STAR) || lines[lineNumber].startsWith(CHECKBOX_UNCHECKED_PLUS)) { + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_MINUS, CHECKBOX_CHECKED_MINUS); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_STAR, CHECKBOX_CHECKED_STAR); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_UNCHECKED_PLUS, CHECKBOX_CHECKED_PLUS); + } else { + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_MINUS, CHECKBOX_UNCHECKED_MINUS); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_STAR, CHECKBOX_UNCHECKED_STAR); + lines[lineNumber] = lines[lineNumber].replace(CHECKBOX_CHECKED_PLUS, CHECKBOX_UNCHECKED_PLUS); + } + + toggleListener.onToggled(TextUtils.join("\n", lines)); + } + + @Override + public void updateDrawState(@NonNull TextPaint ds) { + //NoOp + } + }; + return new Object[]{span, c}; + }); + } + + public interface ToggleListener { + public void onToggled(String newCompmleteText); + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java b/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java new file mode 100644 index 000000000..384dc78c5 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java @@ -0,0 +1,138 @@ +package it.niedermann.android.markdown.markwon.textwatcher; + +import android.text.Editable; +import android.text.TextWatcher; + +import androidx.annotation.NonNull; + +import java.util.concurrent.Executors; + +import io.noties.markwon.editor.MarkwonEditor; +import io.noties.markwon.editor.MarkwonEditorTextWatcher; +import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor; + +/** + * Automatically continues lists and checkbox lists when pressing enter + */ +public class AutoContinuationTextWatcher implements TextWatcher { + + private final MarkwonEditorTextWatcher originalWatcher; + private final MarkwonMarkdownEditor editText; + + private CharSequence customText = null; + private boolean isInsert = true; + private int sequenceStart = 0; + + public AutoContinuationTextWatcher(@NonNull MarkwonEditor editor, @NonNull MarkwonMarkdownEditor editText) { + this.editText = editText; + originalWatcher = MarkwonEditorTextWatcher.withPreRender(editor, Executors.newSingleThreadExecutor(), editText); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + originalWatcher.beforeTextChanged(s, start, count, after); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (count == 1 && s.charAt(start) == '\n') { + handleNewlineInserted(s, start, count); + } + originalWatcher.onTextChanged(s, start, before, count); + } + + @Override + public void afterTextChanged(Editable s) { + if (customText != null) { + final CharSequence customText = this.customText; + this.customText = null; + if (isInsert) { + insertCustomText(s, customText); + } else { + deleteCustomText(s, customText); + } + } else { + originalWatcher.afterTextChanged(s); + } + editText.setMarkdownStringModel(s); + } + + private void deleteCustomText(Editable s, CharSequence customText) { + s.replace(sequenceStart, sequenceStart + customText.length() + 1, "\n"); + editText.setSelection(sequenceStart + 1); + } + + private void insertCustomText(Editable s, CharSequence customText) { + s.insert(sequenceStart, customText); + } + + private void handleNewlineInserted(CharSequence originalSequence, int start, int count) { + final CharSequence s = originalSequence.subSequence(0, originalSequence.length()); + final int startOfLine = getStartOfLine(s, start); + final String line = s.subSequence(startOfLine, start).toString(); + + final String emptyListString = getListItemIfIsEmpty(line); + if (emptyListString != null) { + customText = emptyListString; + isInsert = false; + sequenceStart = startOfLine; + } else { + for (EListType listType : EListType.values()) { + final boolean isCheckboxList = lineStartsWithCheckbox(line, listType); + final boolean isPlainList = !isCheckboxList && lineStartsWithList(line, listType); + if (isPlainList || isCheckboxList) { + customText = isPlainList ? listType.listSymbolWithTrailingSpace : listType.checkboxUncheckedWithTrailingSpace; + isInsert = true; + sequenceStart = start + count; + } + } + } + } + + private static int getStartOfLine(@NonNull CharSequence s, int cursorPosition) { + int startOfLine = cursorPosition; + while (startOfLine > 0 && s.charAt(startOfLine - 1) != '\n') { + startOfLine--; + } + return startOfLine; + } + + private static String getListItemIfIsEmpty(@NonNull String line) { + for (EListType listType : EListType.values()) { + if (line.equals(listType.checkboxUncheckedWithTrailingSpace)) { + return listType.checkboxUncheckedWithTrailingSpace; + } else if (line.equals(listType.listSymbolWithTrailingSpace)) { + return listType.listSymbolWithTrailingSpace; + } + } + return null; + } + + private static boolean lineStartsWithCheckbox(@NonNull String line, @NonNull EListType listType) { + return line.startsWith(listType.checkboxUnchecked) || line.startsWith(listType.checkboxChecked); + } + + private static boolean lineStartsWithList(@NonNull String line, @NonNull EListType listType) { + return line.startsWith(listType.listSymbol); + } + + enum EListType { + STAR('*'), + DASH('-'), + PLUS('+'); + + final String listSymbol; + final String listSymbolWithTrailingSpace; + final String checkboxChecked; + final String checkboxUnchecked; + final String checkboxUncheckedWithTrailingSpace; + + EListType(char listSymbol) { + this.listSymbol = String.valueOf(listSymbol); + this.listSymbolWithTrailingSpace = listSymbol + " "; + this.checkboxChecked = listSymbolWithTrailingSpace + "[x]"; + this.checkboxUnchecked = listSymbolWithTrailingSpace + "[ ]"; + this.checkboxUncheckedWithTrailingSpace = checkboxUnchecked + " "; + } + } +} diff --git a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownEditor.java b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownEditor.java new file mode 100644 index 000000000..1d02d9899 --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownEditor.java @@ -0,0 +1,72 @@ +package it.niedermann.android.markdown.rxmarkdown; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.yydcdut.markdown.MarkdownEditText; +import com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.syntax.edit.EditFactory; + +import it.niedermann.android.markdown.MarkdownEditor; + +import static androidx.lifecycle.Transformations.distinctUntilChanged; + +@Deprecated +public class RxMarkdownEditor extends MarkdownEditText implements MarkdownEditor { + + private final MutableLiveData<CharSequence> unrenderedText$ = new MutableLiveData<>(); + private MarkdownProcessor markdownProcessor; + + public RxMarkdownEditor(Context context) { + super(context); + init(context); + } + + public RxMarkdownEditor(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public RxMarkdownEditor(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + markdownProcessor = new MarkdownProcessor(context); + markdownProcessor.config(RxMarkdownUtil.getMarkDownConfiguration(context).build()); + markdownProcessor.factory(EditFactory.create()); + markdownProcessor.live(this); + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + unrenderedText$.setValue(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + } + + @Override + public void setMarkdownString(CharSequence text) { + setText(markdownProcessor.parse(text)); + } + + @Override + public LiveData<CharSequence> getMarkdownString() { + return distinctUntilChanged(unrenderedText$); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/MarkDownUtil.java b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownUtil.java index 9e1e5b147..16c1652a0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/MarkDownUtil.java +++ b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownUtil.java @@ -1,21 +1,20 @@ -package it.niedermann.nextcloud.deck.util; +package it.niedermann.android.markdown.rxmarkdown; import android.content.Context; -import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; +import androidx.annotation.RestrictTo; import com.yydcdut.rxmarkdown.RxMDConfiguration.Builder; -import it.niedermann.nextcloud.deck.R; - /** * Created by stefan on 07.12.16. */ +@Deprecated +@RestrictTo(value = RestrictTo.Scope.LIBRARY) +public class RxMarkdownUtil { -public class MarkDownUtil { - - private MarkDownUtil() {} + private RxMarkdownUtil() { + } /** * Ensures every instance of RxMD uses the same configuration @@ -30,8 +29,7 @@ public class MarkDownUtil { .setHeader4RelativeSize(1.15f) .setHeader5RelativeSize(1.1f) .setHeader6RelativeSize(1.05f) - .setHorizontalRulesHeight(2) - .setLinkFontColor(ContextCompat.getColor(context, R.color.primary)); + .setHorizontalRulesHeight(2); } public static Builder getMarkDownConfiguration(Context context, Boolean darkTheme) { @@ -41,7 +39,6 @@ public class MarkDownUtil { .setHeader4RelativeSize(1.15f) .setHeader5RelativeSize(1.1f) .setHeader6RelativeSize(1.05f) - .setHorizontalRulesHeight(2) - .setLinkFontColor(ResourcesCompat.getColor(context.getResources(), R.color.primary, null)); + .setHorizontalRulesHeight(2); } } diff --git a/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownViewer.java b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownViewer.java new file mode 100644 index 000000000..3c670d33d --- /dev/null +++ b/markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownViewer.java @@ -0,0 +1,68 @@ +package it.niedermann.android.markdown.rxmarkdown; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; +import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; +import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.yydcdut.markdown.MarkdownProcessor; +import com.yydcdut.markdown.MarkdownTextView; +import com.yydcdut.markdown.syntax.text.TextFactory; + +import java.util.Map; + +import it.niedermann.android.markdown.MarkdownEditor; + +import static it.niedermann.android.markdown.MentionUtil.setupMentions; + +@Deprecated +public class RxMarkdownViewer extends MarkdownTextView implements MarkdownEditor { + + private MarkdownProcessor markdownProcessor; + + public RxMarkdownViewer(Context context) { + super(context); + init(context); + } + + public RxMarkdownViewer(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public RxMarkdownViewer(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + markdownProcessor = new MarkdownProcessor(context); + markdownProcessor.config(RxMarkdownUtil.getMarkDownConfiguration(context).build()); + markdownProcessor.factory(TextFactory.create()); + } + + @Override + public void setMarkdownString(CharSequence text) { + setText(markdownProcessor.parse(text)); + } + + @Override + public LiveData<CharSequence> getMarkdownString() { + return new MutableLiveData<>(); + } + + @Override + public void setMarkdownString(CharSequence text, @NonNull Map<String, String> mentions) { + try { + setMarkdownString(text); + setupMentions(SingleAccountHelper.getCurrentSingleSignOnAccount(getContext()), mentions, this); + } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + e.printStackTrace(); + } + } +} diff --git a/markdown/src/main/res/drawable/ic_person_grey600_24dp.xml b/markdown/src/main/res/drawable/ic_person_grey600_24dp.xml new file mode 100644 index 000000000..23b5b1c10 --- /dev/null +++ b/markdown/src/main/res/drawable/ic_person_grey600_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#757575" + android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" /> +</vector> diff --git a/markdown/src/main/res/values/dimens.xml b/markdown/src/main/res/values/dimens.xml new file mode 100644 index 000000000..57f904297 --- /dev/null +++ b/markdown/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="bullet_point_width">6dp</dimen> +</resources>
\ No newline at end of file diff --git a/markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java b/markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java new file mode 100644 index 000000000..0fa5d2321 --- /dev/null +++ b/markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package it.niedermann.android.markdown; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +}
\ No newline at end of file diff --git a/settings.gradle b/settings.gradle index edf1d2bdb..625f0fb1e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ include ':app' include ':cross-tab-drag-and-drop' include ':tab-layout-helper' +include ':markdown' |