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"> - - - + -