mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-29 18:38:36 +08:00
Move MarkdownRenderer to its own file
Less noise
This commit is contained in:
255
lib/widgets/markdown_renderer.dart
Normal file
255
lib/widgets/markdown_renderer.dart
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:function_types/function_types.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import 'package:gitjournal/core/link.dart';
|
||||||
|
import 'package:gitjournal/core/note.dart';
|
||||||
|
import 'package:gitjournal/folder_views/common.dart';
|
||||||
|
import 'package:gitjournal/utils.dart';
|
||||||
|
import 'package:gitjournal/utils/link_resolver.dart';
|
||||||
|
import 'package:gitjournal/utils/logger.dart';
|
||||||
|
|
||||||
|
class MarkdownRenderer extends StatelessWidget {
|
||||||
|
final Note note;
|
||||||
|
final Func1<Note, void> onNoteTapped;
|
||||||
|
|
||||||
|
const MarkdownRenderer({
|
||||||
|
Key key,
|
||||||
|
@required this.note,
|
||||||
|
@required this.onNoteTapped,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
ThemeData theme = Theme.of(context);
|
||||||
|
theme = theme.copyWith(
|
||||||
|
textTheme: theme.textTheme.copyWith(
|
||||||
|
subtitle1: theme.textTheme.subtitle1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
// Copied from MarkdownStyleSheet except Grey is replaced with Highlight color
|
||||||
|
var markdownStyleSheet = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
||||||
|
code: theme.textTheme.bodyText2.copyWith(
|
||||||
|
backgroundColor: theme.dialogBackgroundColor,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: theme.textTheme.bodyText2.fontSize * 0.85,
|
||||||
|
),
|
||||||
|
tableBorder: TableBorder.all(color: theme.highlightColor, width: 0),
|
||||||
|
tableCellsDecoration: BoxDecoration(color: theme.dialogBackgroundColor),
|
||||||
|
codeblockDecoration: BoxDecoration(
|
||||||
|
color: theme.dialogBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(2.0),
|
||||||
|
),
|
||||||
|
horizontalRuleDecoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(width: 3.0, color: theme.highlightColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
blockquoteDecoration: BoxDecoration(
|
||||||
|
color: theme.primaryColorLight,
|
||||||
|
borderRadius: BorderRadius.circular(2.0),
|
||||||
|
),
|
||||||
|
checkbox: theme.textTheme.bodyText2.copyWith(
|
||||||
|
color: isDark ? theme.primaryColorLight : theme.primaryColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
var view = MarkdownBody(
|
||||||
|
data: note.body,
|
||||||
|
// selectable: false, -> making this true breaks link navigation
|
||||||
|
styleSheet: markdownStyleSheet,
|
||||||
|
onTapLink: (String _, String link, String __) async {
|
||||||
|
final linkResolver = LinkResolver(note);
|
||||||
|
|
||||||
|
var linkedNote = linkResolver.resolve(link);
|
||||||
|
if (linkedNote != null) {
|
||||||
|
onNoteTapped(linkedNote);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LinkResolver.isWikiLink(link)) {
|
||||||
|
var opened =
|
||||||
|
openNewNoteEditor(context, LinkResolver.stripWikiSyntax(link));
|
||||||
|
if (!opened) {
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
tr('widgets.NoteViewer.linkInvalid', args: [link]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// External Link
|
||||||
|
try {
|
||||||
|
await launch(link);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Log.e("Opening Link", ex: e, stacktrace: stackTrace);
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
tr('widgets.NoteViewer.linkNotFound', args: [link]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageBuilder: (url, title, alt) => kDefaultImageBuilder(
|
||||||
|
url, note.parent.folderPath + p.separator, null, null),
|
||||||
|
extensionSet: markdownExtensions(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
static md.ExtensionSet markdownExtensions() {
|
||||||
|
// It's important to add both these inline syntaxes before the other
|
||||||
|
// syntaxes as the LinkSyntax intefers with both of these
|
||||||
|
var markdownExtensions = md.ExtensionSet.gitHubFlavored;
|
||||||
|
markdownExtensions.inlineSyntaxes.insert(0, WikiLinkSyntax());
|
||||||
|
markdownExtensions.inlineSyntaxes.insert(1, TaskListSyntax());
|
||||||
|
return markdownExtensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Widget _buildFooter(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.arrow_left),
|
||||||
|
tooltip: 'Previous Entry',
|
||||||
|
onPressed: showPrevNoteFunc,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 10,
|
||||||
|
child: Text(''),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.arrow_right),
|
||||||
|
tooltip: 'Next Entry',
|
||||||
|
onPressed: showNextNoteFunc,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Copied from flutter_markdown
|
||||||
|
// But it uses CachedNetworkImage
|
||||||
|
//
|
||||||
|
typedef Widget ImageBuilder(
|
||||||
|
Uri uri, String imageDirectory, double width, double height);
|
||||||
|
|
||||||
|
final ImageBuilder kDefaultImageBuilder = (
|
||||||
|
Uri uri,
|
||||||
|
String imageDirectory,
|
||||||
|
double width,
|
||||||
|
double height,
|
||||||
|
) {
|
||||||
|
if (uri.scheme == 'http' || uri.scheme == 'https') {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: uri.toString(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
placeholder: (context, url) => const CircularProgressIndicator(),
|
||||||
|
errorWidget: (context, url, error) => const Icon(Icons.error),
|
||||||
|
);
|
||||||
|
} else if (uri.scheme == 'data') {
|
||||||
|
return _handleDataSchemeUri(uri, width, height);
|
||||||
|
} else if (uri.scheme == "resource") {
|
||||||
|
return Image.asset(uri.path, width: width, height: height);
|
||||||
|
} else {
|
||||||
|
Uri fileUri = imageDirectory != null
|
||||||
|
? Uri.parse(imageDirectory + uri.toString())
|
||||||
|
: uri;
|
||||||
|
if (fileUri.scheme == 'http' || fileUri.scheme == 'https') {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: fileUri.toString(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
placeholder: (context, url) => const CircularProgressIndicator(),
|
||||||
|
errorWidget: (context, url, error) => const Icon(Icons.error),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Image.file(File.fromUri(fileUri), width: width, height: height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Widget _handleDataSchemeUri(Uri uri, final double width, final double height) {
|
||||||
|
final String mimeType = uri.data.mimeType;
|
||||||
|
if (mimeType.startsWith('image/')) {
|
||||||
|
return Image.memory(
|
||||||
|
uri.data.contentAsBytes(),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
} else if (mimeType.startsWith('text/')) {
|
||||||
|
return Text(uri.data.contentAsString());
|
||||||
|
}
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
/// Parse ==Words==
|
||||||
|
class HighlightTermSyntax extends md.InlineSyntax {
|
||||||
|
static final String _pattern = r'==(^=^=*)==';
|
||||||
|
|
||||||
|
HighlightTermSyntax() : super(_pattern);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool onMatch(md.InlineParser parser, Match match) {
|
||||||
|
var displayText = match[1];
|
||||||
|
|
||||||
|
var el = md.Element('span', [md.Text(displayText)]);
|
||||||
|
el.attributes['type'] = 'highlight';
|
||||||
|
|
||||||
|
parser.addNode(el);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
You can't just use this builder as it's mandatory to override visitText
|
||||||
|
which results in links and other rich text elements not being rendered
|
||||||
|
correctly when inside the highlight.
|
||||||
|
You'll need to modify flutter_makrdown to allow such modifications.
|
||||||
|
|
||||||
|
|
||||||
|
class HighlightTermBuilder extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
void visitElementBefore(md.Element element) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget visitText(md.Text text, TextStyle style) {
|
||||||
|
/*
|
||||||
|
style = style.copyWith(backgroundColor: Colors.red);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(text.text, style: style),
|
||||||
|
],
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget visitElementAfter(md.Element element, TextStyle preferredStyle) =>
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
@ -1,24 +1,16 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
|
import 'package:gitjournal/widgets/markdown_renderer.dart';
|
||||||
import 'package:markdown/markdown.dart' as md;
|
import 'package:markdown/markdown.dart' as md;
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
import 'package:gitjournal/core/link.dart';
|
import 'package:gitjournal/core/link.dart';
|
||||||
import 'package:gitjournal/core/note.dart';
|
import 'package:gitjournal/core/note.dart';
|
||||||
import 'package:gitjournal/core/notes_folder.dart';
|
import 'package:gitjournal/core/notes_folder.dart';
|
||||||
import 'package:gitjournal/core/notes_folder_fs.dart';
|
import 'package:gitjournal/core/notes_folder_fs.dart';
|
||||||
import 'package:gitjournal/folder_views/common.dart';
|
import 'package:gitjournal/folder_views/common.dart';
|
||||||
import 'package:gitjournal/utils.dart';
|
|
||||||
import 'package:gitjournal/utils/link_resolver.dart';
|
|
||||||
import 'package:gitjournal/utils/logger.dart';
|
|
||||||
import 'package:gitjournal/widgets/editor_scroll_view.dart';
|
import 'package:gitjournal/widgets/editor_scroll_view.dart';
|
||||||
import 'package:gitjournal/widgets/notes_backlinks.dart';
|
import 'package:gitjournal/widgets/notes_backlinks.dart';
|
||||||
|
|
||||||
@ -33,42 +25,6 @@ class NoteViewer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ThemeData theme = Theme.of(context);
|
|
||||||
theme = theme.copyWith(
|
|
||||||
textTheme: theme.textTheme.copyWith(
|
|
||||||
subtitle1: theme.textTheme.subtitle1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
var isDark = theme.brightness == Brightness.dark;
|
|
||||||
|
|
||||||
// Copied from MarkdownStyleSheet except Grey is replaced with Highlight color
|
|
||||||
var markdownStyleSheet = MarkdownStyleSheet.fromTheme(theme).copyWith(
|
|
||||||
code: theme.textTheme.bodyText2.copyWith(
|
|
||||||
backgroundColor: theme.dialogBackgroundColor,
|
|
||||||
fontFamily: "monospace",
|
|
||||||
fontSize: theme.textTheme.bodyText2.fontSize * 0.85,
|
|
||||||
),
|
|
||||||
tableBorder: TableBorder.all(color: theme.highlightColor, width: 0),
|
|
||||||
tableCellsDecoration: BoxDecoration(color: theme.dialogBackgroundColor),
|
|
||||||
codeblockDecoration: BoxDecoration(
|
|
||||||
color: theme.dialogBackgroundColor,
|
|
||||||
borderRadius: BorderRadius.circular(2.0),
|
|
||||||
),
|
|
||||||
horizontalRuleDecoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
top: BorderSide(width: 3.0, color: theme.highlightColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
blockquoteDecoration: BoxDecoration(
|
|
||||||
color: theme.primaryColorLight,
|
|
||||||
borderRadius: BorderRadius.circular(2.0),
|
|
||||||
),
|
|
||||||
checkbox: theme.textTheme.bodyText2.copyWith(
|
|
||||||
color: isDark ? theme.primaryColorLight : theme.primaryColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final rootFolder = Provider.of<NotesFolderFS>(context);
|
final rootFolder = Provider.of<NotesFolderFS>(context);
|
||||||
var view = EditorScrollView(
|
var view = EditorScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -76,45 +32,10 @@ class NoteViewer extends StatelessWidget {
|
|||||||
NoteTitleHeader(note.title),
|
NoteTitleHeader(note.title),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
||||||
child: MarkdownBody(
|
child: MarkdownRenderer(
|
||||||
data: note.body,
|
note: note,
|
||||||
// selectable: false, -> making this true breaks link navigation
|
onNoteTapped: (note) =>
|
||||||
styleSheet: markdownStyleSheet,
|
openNoteEditor(context, note, parentFolder),
|
||||||
onTapLink: (String _, String link, String __) async {
|
|
||||||
final linkResolver = LinkResolver(note);
|
|
||||||
|
|
||||||
var linkedNote = linkResolver.resolve(link);
|
|
||||||
if (linkedNote != null) {
|
|
||||||
openNoteEditor(context, linkedNote, parentFolder);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LinkResolver.isWikiLink(link)) {
|
|
||||||
var opened = openNewNoteEditor(
|
|
||||||
context, LinkResolver.stripWikiSyntax(link));
|
|
||||||
if (!opened) {
|
|
||||||
showSnackbar(
|
|
||||||
context,
|
|
||||||
tr('widgets.NoteViewer.linkInvalid', args: [link]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// External Link
|
|
||||||
try {
|
|
||||||
await launch(link);
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
Log.e("Opening Link", ex: e, stacktrace: stackTrace);
|
|
||||||
showSnackbar(
|
|
||||||
context,
|
|
||||||
tr('widgets.NoteViewer.linkNotFound', args: [link]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imageBuilder: (url, title, alt) => kDefaultImageBuilder(
|
|
||||||
url, note.parent.folderPath + p.separator, null, null),
|
|
||||||
extensionSet: markdownExtensions(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16.0),
|
const SizedBox(height: 16.0),
|
||||||
@ -181,111 +102,3 @@ class NoteTitleHeader extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Copied from flutter_markdown
|
|
||||||
// But it uses CachedNetworkImage
|
|
||||||
//
|
|
||||||
typedef Widget ImageBuilder(
|
|
||||||
Uri uri, String imageDirectory, double width, double height);
|
|
||||||
|
|
||||||
final ImageBuilder kDefaultImageBuilder = (
|
|
||||||
Uri uri,
|
|
||||||
String imageDirectory,
|
|
||||||
double width,
|
|
||||||
double height,
|
|
||||||
) {
|
|
||||||
if (uri.scheme == 'http' || uri.scheme == 'https') {
|
|
||||||
return CachedNetworkImage(
|
|
||||||
imageUrl: uri.toString(),
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
placeholder: (context, url) => const CircularProgressIndicator(),
|
|
||||||
errorWidget: (context, url, error) => const Icon(Icons.error),
|
|
||||||
);
|
|
||||||
} else if (uri.scheme == 'data') {
|
|
||||||
return _handleDataSchemeUri(uri, width, height);
|
|
||||||
} else if (uri.scheme == "resource") {
|
|
||||||
return Image.asset(uri.path, width: width, height: height);
|
|
||||||
} else {
|
|
||||||
Uri fileUri = imageDirectory != null
|
|
||||||
? Uri.parse(imageDirectory + uri.toString())
|
|
||||||
: uri;
|
|
||||||
if (fileUri.scheme == 'http' || fileUri.scheme == 'https') {
|
|
||||||
return CachedNetworkImage(
|
|
||||||
imageUrl: fileUri.toString(),
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
placeholder: (context, url) => const CircularProgressIndicator(),
|
|
||||||
errorWidget: (context, url, error) => const Icon(Icons.error),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Image.file(File.fromUri(fileUri), width: width, height: height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Widget _handleDataSchemeUri(Uri uri, final double width, final double height) {
|
|
||||||
final String mimeType = uri.data.mimeType;
|
|
||||||
if (mimeType.startsWith('image/')) {
|
|
||||||
return Image.memory(
|
|
||||||
uri.data.contentAsBytes(),
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
);
|
|
||||||
} else if (mimeType.startsWith('text/')) {
|
|
||||||
return Text(uri.data.contentAsString());
|
|
||||||
}
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
/// Parse ==Words==
|
|
||||||
class HighlightTermSyntax extends md.InlineSyntax {
|
|
||||||
static final String _pattern = r'==(^=^=*)==';
|
|
||||||
|
|
||||||
HighlightTermSyntax() : super(_pattern);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool onMatch(md.InlineParser parser, Match match) {
|
|
||||||
var displayText = match[1];
|
|
||||||
|
|
||||||
var el = md.Element('span', [md.Text(displayText)]);
|
|
||||||
el.attributes['type'] = 'highlight';
|
|
||||||
|
|
||||||
parser.addNode(el);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
You can't just use this builder as it's mandatory to override visitText
|
|
||||||
which results in links and other rich text elements not being rendered
|
|
||||||
correctly when inside the highlight.
|
|
||||||
You'll need to modify flutter_makrdown to allow such modifications.
|
|
||||||
|
|
||||||
|
|
||||||
class HighlightTermBuilder extends MarkdownElementBuilder {
|
|
||||||
@override
|
|
||||||
void visitElementBefore(md.Element element) {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget visitText(md.Text text, TextStyle style) {
|
|
||||||
/*
|
|
||||||
style = style.copyWith(backgroundColor: Colors.red);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(text.text, style: style),
|
|
||||||
],
|
|
||||||
);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget visitElementAfter(md.Element element, TextStyle preferredStyle) =>
|
|
||||||
null;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
Reference in New Issue
Block a user