mirror of https://github.com/M66B/FairEmail.git
Experiment: markdown support
This commit is contained in:
parent
8937b3e2a6
commit
3350c11980
|
@ -55,3 +55,4 @@ FairEmail uses parts or all of:
|
|||
* [AG Filters Registry](https://github.com/AdguardTeam/FiltersRegistry). [GNU Lesser General Public License Version 3](https://github.com/AdguardTeam/FiltersRegistry/blob/master/LICENSE).
|
||||
* [Certificate transparency for Android and JVM](https://github.com/appmattus/certificatetransparency). Copyright 2023 Appmattus Limited. [Apache License 2.0](https://github.com/appmattus/certificatetransparency/blob/main/LICENSE.md).
|
||||
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
||||
* [https://github.com/vsch/flexmark-java](flexmark-java). Copyright (c) 2015-2016, Atlassian Pty Ltd. Copyright (c) 2016-2018, Vladimir Schneider. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
||||
|
|
|
@ -134,7 +134,11 @@ android {
|
|||
'LICENSE-2.0.txt',
|
||||
'RELEASE.txt',
|
||||
'DebugProbesKt.bin',
|
||||
'font_metrics.properties'
|
||||
'font_metrics.properties',
|
||||
'META-INF/LICENSE-LGPL-2.1.txt',
|
||||
'META-INF/LICENSE-LGPL-3.txt',
|
||||
'META-INF/LICENSE-W3C-TEST',
|
||||
'META-INF/DEPENDENCIES'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -557,6 +561,7 @@ dependencies {
|
|||
def vcard_version = "0.12.1"
|
||||
def relinker_version = "1.4.5"
|
||||
def markwon_version = "4.6.2"
|
||||
def flexmark_version = "0.64.8"
|
||||
def bouncycastle_version = "1.77"
|
||||
def colorpicker_version = "0.0.15"
|
||||
def overscroll_version = "1.1.1"
|
||||
|
@ -765,6 +770,13 @@ dependencies {
|
|||
// https://mvnrepository.com/artifact/io.noties.markwon/core
|
||||
implementation "io.noties.markwon:core:$markwon_version"
|
||||
implementation "io.noties.markwon:html:$markwon_version"
|
||||
implementation "io.noties.markwon:editor:$markwon_version"
|
||||
|
||||
// https://github.com/vsch/flexmark-java
|
||||
// https://mvnrepository.com/artifact/com.vladsch.flexmark/flexmark
|
||||
//implementation "com.vladsch.flexmark:flexmark:$flexmark_version"
|
||||
implementation "com.vladsch.flexmark:flexmark-ext-tables:$flexmark_version"
|
||||
implementation "com.vladsch.flexmark:flexmark-html2md-converter:$flexmark_version"
|
||||
|
||||
// // https://github.com/QuadFlask/colorpicker
|
||||
//implementation "com.github.QuadFlask:colorpicker:$colorpicker_version"
|
||||
|
|
|
@ -55,3 +55,4 @@ FairEmail uses parts or all of:
|
|||
* [AG Filters Registry](https://github.com/AdguardTeam/FiltersRegistry). [GNU Lesser General Public License Version 3](https://github.com/AdguardTeam/FiltersRegistry/blob/master/LICENSE).
|
||||
* [Certificate transparency for Android and JVM](https://github.com/appmattus/certificatetransparency). Copyright 2023 Appmattus Limited. [Apache License 2.0](https://github.com/appmattus/certificatetransparency/blob/main/LICENSE.md).
|
||||
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
|
||||
* [https://github.com/vsch/flexmark-java](flexmark-java). Copyright (c) 2015-2016, Atlassian Pty Ltd. Copyright (c) 2016-2018, Vladimir Schneider. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
|
||||
|
|
|
@ -136,6 +136,12 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.bottomnavigation.LabelVisibilityMode;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||
import com.vladsch.flexmark.ext.tables.TablesExtension;
|
||||
import com.vladsch.flexmark.html.HtmlRenderer;
|
||||
import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
|
||||
import com.vladsch.flexmark.parser.Parser;
|
||||
import com.vladsch.flexmark.util.data.MutableDataSet;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||
|
@ -227,6 +233,9 @@ import javax.mail.util.ByteArrayDataSource;
|
|||
import biweekly.ICalendar;
|
||||
import biweekly.component.VEvent;
|
||||
import biweekly.property.Organizer;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
|
||||
public class FragmentCompose extends FragmentBase {
|
||||
private enum State {NONE, LOADING, LOADED}
|
||||
|
@ -280,6 +289,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
|
||||
private ContentResolver resolver;
|
||||
private AdapterAttachment adapter;
|
||||
private MarkwonEditorTextWatcher markwonWatcher;
|
||||
|
||||
private boolean autoscroll_editor;
|
||||
private int compose_color;
|
||||
|
@ -291,6 +301,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
private boolean style = false;
|
||||
private boolean media = true;
|
||||
private boolean compact = false;
|
||||
private boolean markdown = false;
|
||||
private int zoom = 0;
|
||||
private boolean nav_color;
|
||||
private boolean lt_enabled;
|
||||
|
@ -1119,6 +1130,19 @@ public class FragmentCompose extends FragmentBase {
|
|||
invalidateOptionsMenu();
|
||||
Helper.setViewsEnabled(view, false);
|
||||
|
||||
// https://noties.io/Markwon/docs/v4/editor/
|
||||
try {
|
||||
final Markwon markwon = Markwon.create(getContext());
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
markwonWatcher = MarkwonEditorTextWatcher.withPreRender(
|
||||
editor,
|
||||
Helper.getParallelExecutor(),
|
||||
etBody);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(ex);
|
||||
markwonWatcher = null;
|
||||
}
|
||||
|
||||
final DB db = DB.getInstance(getContext());
|
||||
|
||||
SimpleCursorAdapter cadapter = new SimpleCursorAdapter(
|
||||
|
@ -1936,6 +1960,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
menu.findItem(R.id.menu_style).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_media).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_compact).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_markdown).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_contact_group).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_manage_android_contacts).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_manage_local_contacts).setEnabled(state == State.LOADED);
|
||||
|
@ -1992,6 +2017,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
boolean send_chips = prefs.getBoolean("send_chips", true);
|
||||
boolean send_dialog = prefs.getBoolean("send_dialog", true);
|
||||
boolean image_dialog = prefs.getBoolean("image_dialog", true);
|
||||
boolean experiments = prefs.getBoolean("experiments", false);
|
||||
|
||||
menu.findItem(R.id.menu_save_drafts).setChecked(save_drafts);
|
||||
menu.findItem(R.id.menu_send_chips).setChecked(send_chips);
|
||||
|
@ -2000,6 +2026,8 @@ public class FragmentCompose extends FragmentBase {
|
|||
menu.findItem(R.id.menu_style).setChecked(style);
|
||||
menu.findItem(R.id.menu_media).setChecked(media);
|
||||
menu.findItem(R.id.menu_compact).setChecked(compact);
|
||||
menu.findItem(R.id.menu_markdown).setChecked(markdown);
|
||||
menu.findItem(R.id.menu_markdown).setVisible(experiments);
|
||||
|
||||
View image = media_bar.findViewById(R.id.menu_image);
|
||||
if (image != null)
|
||||
|
@ -2083,6 +2111,9 @@ public class FragmentCompose extends FragmentBase {
|
|||
} else if (itemId == R.id.menu_compact) {
|
||||
onMenuCompact();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_markdown) {
|
||||
onMenuMarkdown();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_contact_group) {
|
||||
onMenuContactGroup();
|
||||
return true;
|
||||
|
@ -2315,6 +2346,11 @@ public class FragmentCompose extends FragmentBase {
|
|||
setCompact(compact);
|
||||
}
|
||||
|
||||
private void onMenuMarkdown() {
|
||||
markdown = !markdown;
|
||||
onAction(R.id.menu_save, "Markdown");
|
||||
}
|
||||
|
||||
private void setCompact(boolean compact) {
|
||||
bottom_navigation.setLabelVisibilityMode(compact
|
||||
? LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED
|
||||
|
@ -4990,6 +5026,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
args.putString("subject", etSubject.getText().toString().trim());
|
||||
args.putCharSequence("loaded", (Spanned) etBody.getTag());
|
||||
args.putCharSequence("spanned", etBody.getText());
|
||||
args.putBoolean("markdown", markdown);
|
||||
args.putBoolean("signature", cbSignature.isChecked());
|
||||
args.putBoolean("empty", isEmpty());
|
||||
args.putBoolean("notext", notext);
|
||||
|
@ -6171,6 +6208,9 @@ public class FragmentCompose extends FragmentBase {
|
|||
Elements ref = doc.select("div[fairemail=reference]");
|
||||
ref.remove();
|
||||
|
||||
boolean markdown = Boolean.parseBoolean(doc.body().attr("markdown"));
|
||||
args.putBoolean("markdown", markdown);
|
||||
|
||||
File refFile = data.draft.getRefFile(context);
|
||||
if (refFile.exists()) {
|
||||
ref.html(Helper.readText(refFile));
|
||||
|
@ -6243,6 +6283,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
working = data.draft.id;
|
||||
dsn = (data.draft.dsn != null && !EntityMessage.DSN_NONE.equals(data.draft.dsn));
|
||||
encrypt = data.draft.ui_encrypt;
|
||||
markdown = args.getBoolean("markdown");
|
||||
invalidateOptionsMenu();
|
||||
|
||||
subject = data.draft.subject;
|
||||
|
@ -6665,6 +6706,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
String subject = args.getString("subject");
|
||||
Spanned loaded = (Spanned) args.getCharSequence("loaded");
|
||||
Spanned spanned = (Spanned) args.getCharSequence("spanned");
|
||||
boolean markdown = args.getBoolean("markdown");
|
||||
boolean signature = args.getBoolean("signature");
|
||||
boolean empty = args.getBoolean("empty");
|
||||
boolean notext = args.getBoolean("notext");
|
||||
|
@ -6673,7 +6715,22 @@ public class FragmentCompose extends FragmentBase {
|
|||
boolean silent = extras.getBoolean("silent");
|
||||
|
||||
boolean dirty = false;
|
||||
String body = HtmlHelper.toHtml(spanned, context);
|
||||
String body;
|
||||
if (markdown) {
|
||||
MutableDataSet options = new MutableDataSet();
|
||||
options.set(Parser.EXTENSIONS, Arrays.asList(
|
||||
TablesExtension.create(),
|
||||
StrikethroughExtension.create()));
|
||||
Parser parser = Parser.builder(options).build();
|
||||
HtmlRenderer renderer = HtmlRenderer.builder(options).build();
|
||||
String html = renderer.render(parser.parse(spanned.toString()));
|
||||
|
||||
Document doc = JsoupEx.parse(html);
|
||||
doc.body().attr("markdown", "true");
|
||||
body = doc.html();
|
||||
} else
|
||||
body = HtmlHelper.toHtml(spanned, context);
|
||||
|
||||
EntityMessage draft;
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
@ -7581,12 +7638,15 @@ public class FragmentCompose extends FragmentBase {
|
|||
Bundle args = new Bundle();
|
||||
args.putLong("id", draft.id);
|
||||
args.putBoolean("show_images", show_images);
|
||||
args.putBoolean("markdown", markdown);
|
||||
|
||||
new SimpleTask<Spanned[]>() {
|
||||
@Override
|
||||
protected void onPreExecute(Bundle args) {
|
||||
// Needed to get width for images
|
||||
grpBody.setVisibility(View.VISIBLE);
|
||||
if (markwonWatcher != null)
|
||||
etBody.removeTextChangedListener(markwonWatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -7602,12 +7662,18 @@ public class FragmentCompose extends FragmentBase {
|
|||
Helper.setViewsEnabled(view, true);
|
||||
|
||||
invalidateOptionsMenu();
|
||||
|
||||
if (markdown && markwonWatcher != null) {
|
||||
etBody.addTextChangedListener(markwonWatcher);
|
||||
markwonWatcher.afterTextChanged(etBody.getText());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Spanned[] onExecute(final Context context, Bundle args) throws Throwable {
|
||||
final long id = args.getLong("id");
|
||||
final boolean show_images = args.getBoolean("show_images", false);
|
||||
final boolean markdown = args.getBoolean("markdown", false);
|
||||
|
||||
int colorPrimary = Helper.resolveColor(context, androidx.appcompat.R.attr.colorPrimary);
|
||||
final int colorBlockquote = Helper.resolveColor(context, R.attr.colorBlockquote, colorPrimary);
|
||||
|
@ -7624,35 +7690,41 @@ public class FragmentCompose extends FragmentBase {
|
|||
Elements ref = doc.select("div[fairemail=reference]");
|
||||
ref.remove();
|
||||
|
||||
HtmlHelper.clearAnnotations(doc); // Legacy left-overs
|
||||
Spanned spannedBody;
|
||||
if (markdown) {
|
||||
MutableDataSet options = new MutableDataSet();
|
||||
spannedBody = new SpannableStringBuilder(FlexmarkHtmlConverter.builder(options).build().convert(doc.html()));
|
||||
} else {
|
||||
HtmlHelper.clearAnnotations(doc); // Legacy left-overs
|
||||
|
||||
doc = HtmlHelper.sanitizeCompose(context, doc.html(), true);
|
||||
doc = HtmlHelper.sanitizeCompose(context, doc.html(), true);
|
||||
|
||||
Spanned spannedBody = HtmlHelper.fromDocument(context, doc, new HtmlHelper.ImageGetterEx() {
|
||||
@Override
|
||||
public Drawable getDrawable(Element element) {
|
||||
return ImageHelper.decodeImage(context,
|
||||
id, element, true, zoom, 1.0f, etBody);
|
||||
spannedBody = HtmlHelper.fromDocument(context, doc, new HtmlHelper.ImageGetterEx() {
|
||||
@Override
|
||||
public Drawable getDrawable(Element element) {
|
||||
return ImageHelper.decodeImage(context,
|
||||
id, element, true, zoom, 1.0f, etBody);
|
||||
}
|
||||
}, null);
|
||||
|
||||
SpannableStringBuilder bodyBuilder = new SpannableStringBuilderEx(spannedBody);
|
||||
QuoteSpan[] bodySpans = bodyBuilder.getSpans(0, bodyBuilder.length(), QuoteSpan.class);
|
||||
for (QuoteSpan quoteSpan : bodySpans) {
|
||||
QuoteSpan q;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||
q = new QuoteSpan(colorBlockquote);
|
||||
else
|
||||
q = new QuoteSpan(colorBlockquote, quoteStripe, quoteGap);
|
||||
bodyBuilder.setSpan(q,
|
||||
bodyBuilder.getSpanStart(quoteSpan),
|
||||
bodyBuilder.getSpanEnd(quoteSpan),
|
||||
bodyBuilder.getSpanFlags(quoteSpan));
|
||||
bodyBuilder.removeSpan(quoteSpan);
|
||||
}
|
||||
}, null);
|
||||
|
||||
SpannableStringBuilder bodyBuilder = new SpannableStringBuilderEx(spannedBody);
|
||||
QuoteSpan[] bodySpans = bodyBuilder.getSpans(0, bodyBuilder.length(), QuoteSpan.class);
|
||||
for (QuoteSpan quoteSpan : bodySpans) {
|
||||
QuoteSpan q;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||
q = new QuoteSpan(colorBlockquote);
|
||||
else
|
||||
q = new QuoteSpan(colorBlockquote, quoteStripe, quoteGap);
|
||||
bodyBuilder.setSpan(q,
|
||||
bodyBuilder.getSpanStart(quoteSpan),
|
||||
bodyBuilder.getSpanEnd(quoteSpan),
|
||||
bodyBuilder.getSpanFlags(quoteSpan));
|
||||
bodyBuilder.removeSpan(quoteSpan);
|
||||
spannedBody = bodyBuilder;
|
||||
}
|
||||
|
||||
spannedBody = bodyBuilder;
|
||||
|
||||
Spanned spannedRef = null;
|
||||
if (!ref.isEmpty()) {
|
||||
Document dref = JsoupEx.parse(ref.outerHtml());
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,4l0,2l3,0l0,12l-3,0l0,2l5,0l0,-16z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,20l5,0l0,-2l-3,0l0,-12l3,0l0,-2l-5,0z"/>
|
||||
</vector>
|
|
@ -79,6 +79,13 @@
|
|||
android:icon="@drawable/outline_unfold_less_24"
|
||||
android:title="@string/title_compact"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_markdown"
|
||||
android:checkable="true"
|
||||
android:icon="@drawable/twotone_data_array_24"
|
||||
android:title="@string/title_markdown"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
|
||||
<group android:id="@+id/group_operations">
|
||||
|
|
|
@ -1739,6 +1739,7 @@
|
|||
<string name="title_image_dialog">Show image options</string>
|
||||
<string name="title_style_toolbar">Style toolbar</string>
|
||||
<string name="title_media_toolbar">Media toolbar</string>
|
||||
<string name="title_markdown" translatable="false">Markdown</string>
|
||||
<string name="title_manage_android_contacts">Manage Android\'s contacts</string>
|
||||
<string name="title_manage_local_contacts">Manage local contacts</string>
|
||||
<string name="title_insert_contact_group">Insert contact group</string>
|
||||
|
|
Loading…
Reference in New Issue