mirror of
https://codeberg.org/gitnex/GitNex
synced 2025-03-08 23:57:44 +01:00
Improve md rendering (#1008)
* the first commits close https://codeberg.org/gitnex/GitNex/issues/925 * instead of using `TextView`s, it now uses `RecyclerView`s as they are supported by `Markwon` too and especially tables look better * see https://noties.io/Markwon/docs/v4/recycler/ and https://noties.io/Markwon/docs/v4/recycler-table/ * I replaced the `TextView`s on issue descriptions, comments, file viewer and the README viewer with a `RecyclerView` * the second parts close https://codeberg.org/gitnex/GitNex/issues/993 * images are now displayed if content is rendered using a `RecyclerView` * it seems that there is an issue with the `PicassoImagesPlugin` with `TextView`s, with `RecyclerView`s it's working * the third parts render issue/PR links like #1 as links, that closes https://codeberg.org/gitnex/GitNex/issues/72 * therefore, I added an URL prefix to the deep links named `gitnex` so that it is possible to open links from any instance by using `gitnex` instead of `http`/`https` * Full links are rendered as #index too * code is mostly from the sample app (https://github.com/noties/Markwon/blob/master/app-sample/src/main/java/io/noties/markwon/app/samples/GithubUserIssueInlineParsingSample.java#L60-L110) * I undid https://codeberg.org/gitnex/GitNex/pulls/995 because it wouldn't work if you have code block (starting/ending with ```) with newlines in it, I found another solution (see [the issue on gh](https://github.com/noties/Markwon/issues/168#issuecomment-622943057) andef9bdbfb90
) * in the next commits (dd99f435ee...2021a71951
), I fixed and improved various things * commit links are only rendered as short SHA * supports relative attachment links (addresses #993) * don't render email addresses as user mentions However, one thing isn't working right now: * the `LinkifyPlugin` is not working with `RecyclerView`s right now * I would like to change the way how the links are rendered and to open the activities directly instead of opening the deeplinkactivity close https://codeberg.org/gitnex/GitNex/issues/925 close https://codeberg.org/gitnex/GitNex/issues/993 close https://codeberg.org/gitnex/GitNex/issues/72 Co-authored-by: qwerty287 <ndev@web.de> Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/1008 Reviewed-by: 6543 <6543@noreply.codeberg.org> Co-authored-by: qwerty287 <qwerty287@noreply.codeberg.org> Co-committed-by: qwerty287 <qwerty287@noreply.codeberg.org>
This commit is contained in:
parent
c2c8d111e4
commit
dec9c1e224
@ -189,6 +189,13 @@
|
||||
<data android:host="opendev.org" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="gitnex" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<!-- deep links -->
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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_ = "<font color='" + ResourcesCompat.getColor(getResources(), R.color.lightGray, null) + "'>" + appCtx.getResources()
|
||||
.getString(R.string.hash) + singleIssue.getNumber() + "</font>";
|
||||
viewBinding.issueTitle.setText(HtmlCompat.fromHtml(issueNumber_ + " " + EmojiParser.parseToUnicode(singleIssue.getTitle()), HtmlCompat.FROM_HTML_MODE_LEGACY));
|
||||
String cleanIssueDescription = singleIssue.getBody().trim().replace("\n", "<br/>");
|
||||
String cleanIssueDescription = singleIssue.getBody().trim();
|
||||
|
||||
viewBinding.assigneeAvatar.setOnClickListener(loginId -> {
|
||||
Intent intent = new Intent(ctx, ProfileActivity.class);
|
||||
|
@ -74,7 +74,7 @@ public class IssueCommentsAdapter extends RecyclerView.Adapter<IssueCommentsAdap
|
||||
private final ImageView avatar;
|
||||
private final TextView author;
|
||||
private final TextView information;
|
||||
private final TextView comment;
|
||||
private final RecyclerView comment;
|
||||
private final LinearLayout commentReactionBadges;
|
||||
|
||||
private IssueCommentViewHolder(View view) {
|
||||
|
@ -3,10 +3,17 @@ package org.mian.gitnex.helpers;
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.InlineParserFactory;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.mian.gitnex.R;
|
||||
import org.mian.gitnex.clients.PicassoService;
|
||||
import org.mian.gitnex.core.MainGrammarLocator;
|
||||
@ -15,16 +22,27 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||
import io.noties.markwon.core.CorePlugin;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.ext.tables.TableAwareMovementMethod;
|
||||
import io.noties.markwon.ext.tables.TablePlugin;
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||
import io.noties.markwon.html.HtmlPlugin;
|
||||
import io.noties.markwon.image.picasso.PicassoImagesPlugin;
|
||||
import io.noties.markwon.inlineparser.InlineProcessor;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.noties.markwon.movement.MovementMethodPlugin;
|
||||
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||
import io.noties.markwon.recycler.SimpleEntry;
|
||||
import io.noties.markwon.recycler.table.TableEntry;
|
||||
import io.noties.markwon.recycler.table.TableEntryPlugin;
|
||||
import io.noties.markwon.syntax.Prism4jTheme;
|
||||
import io.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||
import io.noties.markwon.syntax.Prism4jThemeDefault;
|
||||
@ -54,6 +72,7 @@ public class Markdown {
|
||||
new ThreadPoolExecutor(MAX_POOL_SIZE / 2, MAX_POOL_SIZE, MAX_THREAD_KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>());
|
||||
|
||||
private static final Pool<Renderer> rendererPool;
|
||||
private static final Pool<RecyclerViewRenderer> rvRendererPool;
|
||||
|
||||
static {
|
||||
|
||||
@ -75,12 +94,29 @@ public class Markdown {
|
||||
|
||||
rendererPool = new BlazePool<>(config);
|
||||
|
||||
Config<RecyclerViewRenderer> configRv = new Config<>();
|
||||
|
||||
configRv.setBackgroundExpirationEnabled(true);
|
||||
configRv.setPreciseLeakDetectionEnabled(true);
|
||||
configRv.setSize(MAX_POOL_SIZE);
|
||||
configRv.setAllocator(new Allocator<RecyclerViewRenderer>() {
|
||||
|
||||
@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("((?<!]\\(|`)" + instanceUrl + "[^/]+/[^/]+/(?:issues|pulls)/\\d+)(?:/#|#)issuecomment-(\\d+)(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherComment = patternComment.matcher(markdown);
|
||||
markdown = matcherComment.replaceAll("$1 ([" + context.getString(R.string.commentButtonText) + "]($1#issuecomment-$2))");
|
||||
|
||||
// second step: remove links to issue descriptions
|
||||
final Pattern patternIssueDesc = Pattern.compile("((?<!]\\(|`)" + instanceUrl + "[^/]+/[^/]+/(?:issues|pulls)/\\d+)(?:/#|#)issue-(\\d+)(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherIssueDesc = patternIssueDesc.matcher(markdown);
|
||||
markdown = matcherIssueDesc.replaceAll("$1");
|
||||
|
||||
// third step: replace issue links from the same repo
|
||||
final Pattern pattern = Pattern.compile("(?<!]\\(|`)" + instanceUrl + tinyDB.getString("repoFullName") + "/(?:issues|pulls)/(\\d+)(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcher = pattern.matcher(markdown);
|
||||
markdown = matcher.replaceAll("#$1");
|
||||
|
||||
// fourth step: replace issue links from other repos
|
||||
String substOtherRepo =
|
||||
"[$2/$3#$4](" + instanceUrl.replace("http://", "gitnex://").replace("http://", "gitnex://") + "$1)";
|
||||
final Pattern patternOtherRepo = Pattern.compile("(?<!]\\(|`)" + instanceUrl + "(([^/]+)/([^/]+)/(?:issues|pulls)/(\\d+))(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherOtherRepo = patternOtherRepo.matcher(markdown);
|
||||
markdown = matcherOtherRepo.replaceAll(substOtherRepo);
|
||||
|
||||
// fifth step: render commit links
|
||||
String substCommit =
|
||||
"[$2](" + instanceUrl.replace("http://", "gitnex://").replace("http://", "gitnex://") + "$1)";
|
||||
final Pattern patternCommit = Pattern.compile("(?<!]\\(|`)" + instanceUrl + "([^/]+/[^/]+/commit/([a-z0-9_]+))(?!`|\\)|\\S+)", Pattern.MULTILINE);
|
||||
final Matcher matcherCommit = patternCommit.matcher(markdown);
|
||||
markdown = matcherCommit.replaceAll(substCommit);
|
||||
|
||||
// sixth step: replace relative attachment links
|
||||
String substAttachments =
|
||||
instanceUrl + tinyDB.getString("repoFullName") + "/$1";
|
||||
final Pattern patternAttachments = Pattern.compile("(?<=\\()/(attachments/\\S+)(?=\\))", Pattern.MULTILINE); // TODO code block ``
|
||||
final Matcher matcherAttachments = patternAttachments.matcher(markdown);
|
||||
markdown = matcherAttachments.replaceAll(substAttachments);
|
||||
|
||||
this.context = context;
|
||||
this.markdown = markdown;
|
||||
this.recyclerView = recyclerView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
Objects.requireNonNull(context);
|
||||
Objects.requireNonNull(markdown);
|
||||
Objects.requireNonNull(recyclerView);
|
||||
|
||||
if(markwon == null) setup();
|
||||
|
||||
setupAdapter();
|
||||
|
||||
RecyclerView localReference = recyclerView;
|
||||
String localMd = markdown;
|
||||
MarkwonAdapter localAdapter = adapter;
|
||||
localReference.post(() -> {
|
||||
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("(?<!\\S)(?<=@)\\w+");
|
||||
|
||||
@Override
|
||||
public char specialCharacter() {
|
||||
return '@';
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Node parse() {
|
||||
final String user = match(RE);
|
||||
if (user != null) {
|
||||
final Link link = new Link(createUserLinkDestination(user, context), null);
|
||||
link.appendChild(text("@" + user));
|
||||
return link;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String createUserLinkDestination(@NonNull String user, 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 + user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@
|
||||
style="@style/Widget.MaterialComponents.LinearProgressIndicator"
|
||||
app:indicatorColor="?attr/progressIndicatorColor" />
|
||||
|
||||
<ScrollView
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryBackgroundColor">
|
||||
@ -77,7 +77,7 @@
|
||||
android:padding="16dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/markdown"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -85,6 +85,15 @@
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/markdownTv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/primaryTextColor"
|
||||
android:textIsSelectable="true"
|
||||
android:visibility="gone"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.mian.gitnex.helpers.views.SyntaxHighlightedArea
|
||||
@ -103,6 +112,6 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -77,7 +77,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ScrollView
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/scrollViewComments"
|
||||
@ -165,7 +165,7 @@
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/issueDescription"
|
||||
android:layout_below="@+id/assigneesScrollView"
|
||||
android:layout_width="match_parent"
|
||||
@ -262,7 +262,7 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
6
app/src/main/res/layout/custom_markdown_adapter.xml
Normal file
6
app/src/main/res/layout/custom_markdown_adapter.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_height="wrap_content" />
|
18
app/src/main/res/layout/custom_markdown_code_block.xml
Normal file
18
app/src/main/res/layout/custom_markdown_code_block.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:scrollbarStyle="outsideInset"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingBottom="8dip" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textCodeBlock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingExtra="2dip"/>
|
||||
|
||||
</HorizontalScrollView>
|
19
app/src/main/res/layout/custom_markdown_table.xml
Normal file
19
app/src/main/res/layout/custom_markdown_table.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/table_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="*" />
|
||||
|
||||
</HorizontalScrollView>
|
@ -5,7 +5,7 @@
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ScrollView
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/primaryBackgroundColor"
|
||||
android:layout_width="match_parent"
|
||||
@ -385,7 +385,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/repoFileContents"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -397,7 +397,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_bar"
|
||||
|
@ -63,7 +63,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/comment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
Loading…
x
Reference in New Issue
Block a user