diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f89ac9c6..b361a611 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -189,6 +189,13 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java b/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java
index 3492fcf6..bd77091b 100644
--- a/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java
@@ -175,15 +175,17 @@ public class FileViewActivity extends BaseActivity implements BottomSheetListene
binding.contents.setVisibility(View.GONE);
binding.markdownFrame.setVisibility(View.VISIBLE);
- binding.markdown.setText(getString(R.string.excludeFilesInFileViewer));
- binding.markdown.setGravity(Gravity.CENTER);
- binding.markdown.setTypeface(null, Typeface.BOLD);
+ binding.markdown.setVisibility(View.GONE);
+ binding.markdownTv.setVisibility(View.VISIBLE);
+ binding.markdownTv.setText(getString(R.string.excludeFilesInFileViewer));
+ binding.markdownTv.setGravity(Gravity.CENTER);
+ binding.markdownTv.setTypeface(null, Typeface.BOLD);
});
}
} else {
runOnUiThread(() -> {
- binding.markdown.setText("");
+ binding.markdownTv.setText("");
binding.progressBar.setVisibility(View.GONE);
});
}
@@ -256,7 +258,9 @@ public class FileViewActivity extends BaseActivity implements BottomSheetListene
} else if(id == R.id.markdown) {
if(!tinyDB.getBoolean("enableMarkdownInFileView")) {
- Markdown.render(ctx, EmojiParser.parseToUnicode(binding.contents.getContent()), binding.markdown);
+ if(binding.markdown.getAdapter() == null) {
+ Markdown.render(ctx, EmojiParser.parseToUnicode(binding.contents.getContent()), binding.markdown);
+ }
binding.contents.setVisibility(View.GONE);
binding.markdownFrame.setVisibility(View.VISIBLE);
diff --git a/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java b/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
index c4e98006..dc937fbe 100644
--- a/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
@@ -23,6 +23,7 @@ import android.widget.ScrollView;
import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.text.HtmlCompat;
+import androidx.core.widget.NestedScrollView;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -143,7 +144,7 @@ public class IssueDetailActivity extends BaseActivity implements LabelsListAdapt
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- viewBinding.scrollViewComments.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+ viewBinding.scrollViewComments.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
if((scrollY - oldScrollY) > 0 && viewBinding.addNewComment.isShown()) {
viewBinding.addNewComment.setVisibility(View.GONE);
@@ -590,7 +591,7 @@ public class IssueDetailActivity extends BaseActivity implements LabelsListAdapt
String issueNumber_ = "" + appCtx.getResources()
.getString(R.string.hash) + singleIssue.getNumber() + "";
viewBinding.issueTitle.setText(HtmlCompat.fromHtml(issueNumber_ + " " + EmojiParser.parseToUnicode(singleIssue.getTitle()), HtmlCompat.FROM_HTML_MODE_LEGACY));
- String cleanIssueDescription = singleIssue.getBody().trim().replace("\n", "
");
+ String cleanIssueDescription = singleIssue.getBody().trim();
viewBinding.assigneeAvatar.setOnClickListener(loginId -> {
Intent intent = new Intent(ctx, ProfileActivity.class);
diff --git a/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java
index 7e18e9be..82581553 100644
--- a/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java
+++ b/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java
@@ -74,7 +74,7 @@ public class IssueCommentsAdapter extends RecyclerView.Adapter());
private static final Pool rendererPool;
+ private static final Pool rvRendererPool;
static {
@@ -75,12 +94,29 @@ public class Markdown {
rendererPool = new BlazePool<>(config);
+ Config configRv = new Config<>();
+
+ configRv.setBackgroundExpirationEnabled(true);
+ configRv.setPreciseLeakDetectionEnabled(true);
+ configRv.setSize(MAX_POOL_SIZE);
+ configRv.setAllocator(new Allocator() {
+
+ @Override
+ public RecyclerViewRenderer allocate(Slot slot) {
+ return new RecyclerViewRenderer(slot);
+ }
+
+ @Override public void deallocate(RecyclerViewRenderer poolable) {}
+
+ });
+
+ rvRendererPool = new BlazePool<>(configRv);
+
}
public static void render(Context context, String markdown, TextView textView) {
try {
- textView.setMovementMethod(LinkMovementMethod.getInstance());
Renderer renderer = rendererPool.claim(timeout);
if(renderer != null) {
@@ -90,6 +126,18 @@ public class Markdown {
} catch(InterruptedException ignored) {}
}
+ public static void render(Context context, String markdown, RecyclerView recyclerView) {
+
+ try {
+ RecyclerViewRenderer renderer = rvRendererPool.claim(timeout);
+
+ if(renderer != null) {
+ renderer.setParameters(context, markdown, recyclerView);
+ executorService.execute(renderer);
+ }
+ } catch(InterruptedException ignored) {}
+ }
+
private static class Renderer implements Runnable, Poolable {
private final Slot slot;
@@ -114,13 +162,38 @@ public class Markdown {
.usePlugin(CorePlugin.create())
.usePlugin(HtmlPlugin.create())
.usePlugin(LinkifyPlugin.create(true))
+ .usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(TablePlugin.create(context))
+ .usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(PicassoImagesPlugin.create(PicassoService.getInstance(context).get()))
.usePlugin(SyntaxHighlightPlugin.create(new Prism4j(MainGrammarLocator.getInstance()), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE))
.usePlugin(new AbstractMarkwonPlugin() {
+ private Typeface tf;
+
+ private void setupTf(Context context) {
+ switch(TinyDB.getInstance(context).getInt("customFontId", -1)) {
+ case 0:
+ tf = Typeface.createFromAsset(context.getAssets(), "fonts/roboto.ttf");
+ break;
+ case 2:
+ tf = Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf");
+ break;
+ default:
+ tf = Typeface.createFromAsset(context.getAssets(), "fonts/manroperegular.ttf");
+ break;
+ }
+ }
+
+ @Override
+ public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
+ if(tf == null) setupTf(textView.getContext());
+ textView.setTypeface(tf);
+ super.beforeSetText(textView, markdown);
+ }
+
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder.codeBlockTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
@@ -129,11 +202,13 @@ public class Markdown {
builder.codeTextSize((int) (context.getResources().getDisplayMetrics().scaledDensity * 13));
builder.codeTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
builder.linkColor(ResourcesCompat.getColor(context.getResources(), R.color.lightBlue, null));
+
+ if(tf == null) setupTf(context);
+ builder.headingTypeface(tf);
}
});
markwon = builder.build();
-
}
public void setParameters(Context context, String markdown, TextView textView) {
@@ -176,4 +251,267 @@ public class Markdown {
slot.expire(this);
}
}
+
+ private static class RecyclerViewRenderer implements Runnable, Poolable {
+
+ private final Slot slot;
+
+ private Markwon markwon;
+
+ private Context context;
+ private String markdown;
+ private RecyclerView recyclerView;
+ private MarkwonAdapter adapter;
+
+ public RecyclerViewRenderer(Slot slot) {
+ this.slot = slot;
+ }
+
+ private void setup() {
+
+ Objects.requireNonNull(context);
+
+ Prism4jTheme prism4jTheme = TinyDB.getInstance(context).getString("currentTheme").equals("dark") ?
+ Prism4jThemeDarkula.create() :
+ Prism4jThemeDefault.create();
+
+ final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
+ .addInlineProcessor(new IssueInlineProcessor(context))
+ .addInlineProcessor(new UserInlineProcessor(context))
+ .build();
+
+ Markwon.Builder builder = Markwon.builder(context)
+ .usePlugin(CorePlugin.create())
+ .usePlugin(HtmlPlugin.create())
+ .usePlugin(LinkifyPlugin.create(true)) // TODO not working
+ .usePlugin(SoftBreakAddsNewLinePlugin.create())
+ .usePlugin(TableEntryPlugin.create(context))
+ .usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
+ .usePlugin(TaskListPlugin.create(context))
+ .usePlugin(StrikethroughPlugin.create())
+ .usePlugin(PicassoImagesPlugin.create(PicassoService.getInstance(context).get()))
+ .usePlugin(SyntaxHighlightPlugin.create(new Prism4j(MainGrammarLocator.getInstance()), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE))
+ .usePlugin(new AbstractMarkwonPlugin() {
+
+ private Typeface tf;
+
+ private void setupTf(Context context) {
+ switch(TinyDB.getInstance(context).getInt("customFontId", -1)) {
+ case 0:
+ tf = Typeface.createFromAsset(context.getAssets(), "fonts/roboto.ttf");
+ break;
+ case 2:
+ tf = Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf");
+ break;
+ default:
+ tf = Typeface.createFromAsset(context.getAssets(), "fonts/manroperegular.ttf");
+ break;
+ }
+ }
+
+ @Override
+ public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
+ if(tf == null) setupTf(textView.getContext());
+ textView.setTypeface(tf);
+ super.beforeSetText(textView, markdown);
+ }
+
+ @Override
+ public void configureParser(@NonNull Parser.Builder builder) {
+ builder.inlineParserFactory(inlineParserFactory);
+ }
+
+ @Override
+ public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
+ builder.codeBlockTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
+ builder.codeBlockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
+ builder.blockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
+ builder.codeTextSize((int) (context.getResources().getDisplayMetrics().scaledDensity * 13));
+ builder.codeTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
+ builder.linkColor(ResourcesCompat.getColor(context.getResources(), R.color.lightBlue, null));
+
+ if(tf == null) setupTf(context);
+ builder.headingTypeface(Typeface.create(tf, Typeface.BOLD));
+ }
+ });
+
+ markwon = builder.build();
+ }
+
+ private void setupAdapter() {
+ adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.custom_markdown_adapter)
+ .include(TableBlock.class, TableEntry.create(builder2 -> builder2
+ .tableLayout(R.layout.custom_markdown_table, R.id.table_layout)
+ .textLayoutIsRoot(R.layout.custom_markdown_adapter)))
+ .include(FencedCodeBlock.class, SimpleEntry.create(R.layout.custom_markdown_code_block, R.id.textCodeBlock))
+ .build();
+ }
+
+ public void setParameters(Context context, String markdown, RecyclerView recyclerView) {
+ TinyDB tinyDB = TinyDB.getInstance(context);
+ String instanceUrl = tinyDB.getString("instanceUrl");
+ instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/")).replaceAll("\\.", "\\.");
+
+ // first step: replace comment urls with {url without comment} (comment)
+ final Pattern patternComment = Pattern.compile("((? {
+ localReference.setLayoutManager(new LinearLayoutManager(context) {
+ @Override
+ public boolean canScrollVertically() {
+ return false; // disable RecyclerView scrolling, handeled by seperate ScrollViews
+ }
+ });
+ localReference.setAdapter(localAdapter);
+
+ localAdapter.setMarkdown(markwon, localMd);
+ localAdapter.notifyDataSetChanged();
+ });
+
+ release();
+
+ }
+
+ @Override
+ public void release() {
+
+ context = null;
+ markdown = null;
+ recyclerView = null;
+ adapter = null;
+
+ slot.release(this);
+
+ }
+
+ public void expire() {
+ slot.expire(this);
+ }
+ }
+
+ private static class IssueInlineProcessor extends InlineProcessor {
+
+ private final Context context;
+
+ public IssueInlineProcessor(Context context) {
+ this.context = context;
+ }
+
+ private static final Pattern RE = Pattern.compile("(?<=#)\\d+");
+
+ @Override
+ public char specialCharacter() {
+ return '#';
+ }
+
+ @Override
+ protected Node parse() {
+ final String id = match(RE);
+ if (id != null) {
+ final Link link = new Link(createIssueOrPullRequestLinkDestination(id, context), null);
+ link.appendChild(text("#" + id));
+ return link;
+ }
+ return null;
+ }
+
+ @NonNull
+ private static String createIssueOrPullRequestLinkDestination(@NonNull String id, Context context) {
+ String instanceUrl = TinyDB.getInstance(context).getString("instanceUrl");
+ instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/"));
+ instanceUrl = instanceUrl.replace("http://", "gitnex://");
+ instanceUrl = instanceUrl.replace("https://", "gitnex://");
+
+ return instanceUrl + TinyDB.getInstance(context).getString("repoFullName") + "/issues/" + id;
+ }
+ }
+
+ private static class UserInlineProcessor extends InlineProcessor {
+
+ private final Context context;
+
+ public UserInlineProcessor(Context context) {
+ this.context = context;
+ }
+
+ private static final Pattern RE = Pattern.compile("(?
-
@@ -77,7 +77,7 @@
android:padding="16dp"
android:visibility="gone">
-
+
+
-
+
diff --git a/app/src/main/res/layout/activity_issue_detail.xml b/app/src/main/res/layout/activity_issue_detail.xml
index 27ededef..153598f8 100644
--- a/app/src/main/res/layout/activity_issue_detail.xml
+++ b/app/src/main/res/layout/activity_issue_detail.xml
@@ -77,7 +77,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
-
-
-
+
diff --git a/app/src/main/res/layout/custom_markdown_adapter.xml b/app/src/main/res/layout/custom_markdown_adapter.xml
new file mode 100644
index 00000000..f0092b26
--- /dev/null
+++ b/app/src/main/res/layout/custom_markdown_adapter.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/app/src/main/res/layout/custom_markdown_code_block.xml b/app/src/main/res/layout/custom_markdown_code_block.xml
new file mode 100644
index 00000000..68c142fb
--- /dev/null
+++ b/app/src/main/res/layout/custom_markdown_code_block.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/custom_markdown_table.xml b/app/src/main/res/layout/custom_markdown_table.xml
new file mode 100644
index 00000000..e4fff5a2
--- /dev/null
+++ b/app/src/main/res/layout/custom_markdown_table.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_repo_info.xml b/app/src/main/res/layout/fragment_repo_info.xml
index eeb48a1a..e5b685be 100644
--- a/app/src/main/res/layout/fragment_repo_info.xml
+++ b/app/src/main/res/layout/fragment_repo_info.xml
@@ -5,7 +5,7 @@
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
-
-
-
+
-