Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stefan-niedermann/nextcloud-deck.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordesperateCoder <echotodevnull@gmail.com>2020-12-10 18:37:16 +0300
committerdesperateCoder <echotodevnull@gmail.com>2020-12-10 18:37:16 +0300
commit044bdc5ae949890284bccf419c155ae817641ec1 (patch)
treea5bac46a4b0e84cc41a366599077cc7bf669792e
parent191e16973a362ac2b488d33e634cee7117d85635 (diff)
parent92bf6199255f7abe472e84c683c7acb7d1a8c582 (diff)
merged origin/master into branch
-rw-r--r--app/build.gradle8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java20
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutActivity.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java27
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java13
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java56
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsActivity.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java62
-rw-r--r--app/src/main/res/drawable/ic_baseline_eye_24.xml5
-rw-r--r--app/src/main/res/layout/fragment_card_edit_tab_comments.xml2
-rw-r--r--app/src/main/res/layout/fragment_card_edit_tab_details.xml47
-rw-r--r--app/src/main/res/layout/item_comment.xml2
-rw-r--r--app/src/main/res/values-hr/strings.xml5
-rw-r--r--app/src/main/res/values/dimens.xml2
-rw-r--r--app/src/main/res/values/strings.xml1
-rw-r--r--fastlane/metadata/android/en-US/changelogs/1013001.txt3
-rw-r--r--markdown/.gitignore1
-rw-r--r--markdown/build.gradle68
-rw-r--r--markdown/consumer-rules.pro0
-rw-r--r--markdown/proguard-rules.pro21
-rw-r--r--markdown/src/androidTest/java/it/niedermann/android/markdown/ExampleInstrumentedTest.java26
-rw-r--r--markdown/src/main/AndroidManifest.xml4
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditor.java34
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/MarkdownEditorImpl.java23
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/MarkdownViewerImpl.java24
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/MentionUtil.java95
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownEditor.java70
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownUtil.java65
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/MarkwonMarkdownViewer.java72
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/BlockQuoteEditHandler.java50
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeBlockEditHandler.java47
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/CodeEditHandler.java54
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/HeadingEditHandler.java142
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/LinkEditHandler.java86
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/handler/StrikethroughEditHandler.java45
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/NextcloudMentionsPlugin.java44
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ThemePlugin.java32
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/plugins/ToggleableTaskListPlugin.java109
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/markwon/textwatcher/AutoContinuationTextWatcher.java138
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownEditor.java72
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownUtil.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/util/MarkDownUtil.java)21
-rw-r--r--markdown/src/main/java/it/niedermann/android/markdown/rxmarkdown/RxMarkdownViewer.java68
-rw-r--r--markdown/src/main/res/drawable/ic_person_grey600_24dp.xml10
-rw-r--r--markdown/src/main/res/values/dimens.xml4
-rw-r--r--markdown/src/test/java/it/niedermann/android/markdown/ExampleUnitTest.java17
-rw-r--r--settings.gradle1
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'