From 5795413f25d40febfc9bbe3d699c61f7464fac50 Mon Sep 17 00:00:00 2001 From: Rongjian Zhang Date: Fri, 6 Nov 2020 18:47:56 +0800 Subject: [PATCH] feat: add markdown render engine setting closes #124 --- lib/models/theme.dart | 43 +++++++++++++++++++++++--- lib/screens/gh_repo.dart | 46 ++++++++++++++-------------- lib/screens/gl_project.dart | 55 ++++++++++++++++------------------ lib/screens/settings.dart | 22 ++++++++++++++ lib/utils/utils.dart | 1 + lib/widgets/markdown_view.dart | 48 +++++++++++++++++++++++++++++ 6 files changed, 158 insertions(+), 57 deletions(-) diff --git a/lib/models/theme.dart b/lib/models/theme.dart index e172cab..9cacb39 100644 --- a/lib/models/theme.dart +++ b/lib/models/theme.dart @@ -33,6 +33,12 @@ class AppBrightnessType { ]; } +class AppMarkdownType { + static const flutter = 0; + static const webview = 1; + static const values = [AppMarkdownType.flutter, AppMarkdownType.webview]; +} + class PickerItem { final T value; final String text; @@ -99,12 +105,12 @@ class Palette { } class ThemeModel with ChangeNotifier { + String markdownCss; + int _theme; int get theme => _theme; bool get ready => _theme != null; - String markdownCss; - Brightness systemBrightness = Brightness.light; void setSystemBrightness(Brightness v) { if (v != systemBrightness) { @@ -138,6 +144,30 @@ class ThemeModel with ChangeNotifier { notifyListeners(); } + // markdown render engine + int _markdown; + int get markdown => _markdown; + Future setMarkdown(int v) async { + _markdown = v; + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(StorageKeys.markdown, v); + Fimber.d('write markdown engine: $v'); + notifyListeners(); + } + + Future getMarkdownFuture( + BuildContext context, { + @required Future Function() md, + @required Future Function() html, + }) { + switch (markdown) { + case AppMarkdownType.webview: + return html(); + default: + return md(); + } + } + final router = FluroRouter(); final paletteLight = Palette( @@ -170,7 +200,10 @@ class ThemeModel with ChangeNotifier { } Future init() async { + markdownCss = await rootBundle.loadString('images/github-markdown.css'); + final prefs = await SharedPreferences.getInstance(); + final v = prefs.getInt(StorageKeys.iTheme); Fimber.d('read theme: $v'); if (AppThemeType.values.contains(v)) { @@ -185,8 +218,10 @@ class ThemeModel with ChangeNotifier { if (AppBrightnessType.values.contains(b)) { _brightnessValue = b; } - - markdownCss = await rootBundle.loadString('images/github-markdown.css'); + final m = prefs.getInt(StorageKeys.markdown); + if (AppMarkdownType.values.contains(m)) { + _markdown = m; + } notifyListeners(); } diff --git a/lib/screens/gh_repo.dart b/lib/screens/gh_repo.dart index e89cc14..f93faa5 100644 --- a/lib/screens/gh_repo.dart +++ b/lib/screens/gh_repo.dart @@ -53,7 +53,7 @@ class GhRepoScreen extends StatelessWidget { Widget build(BuildContext context) { final theme = Provider.of(context); return RefreshStatefulScaffold< - Tuple3, Future>>( + Tuple3, MarkdownViewData>>( title: AppBarTitle('Repository'), fetch: () async { final ghClient = context.read().ghClient; @@ -64,18 +64,27 @@ class GhRepoScreen extends StatelessWidget { .getJSON('/repos/$owner/$name/stats/contributors') .then((v) => (v as List).length); - final readmeFuture = ghClient.request( - 'GET', - '/repos/$owner/$name/readme', - headers: {HttpHeaders.acceptHeader: 'application/vnd.github.v3.html'}, - ).then((res) { - return res.body; - }).catchError((err) { - // 404 - return null; - }); + final readmeFactory = (String acceptHeader) { + return () { + return ghClient.request( + 'GET', + '/repos/$owner/$name/readme', + headers: {HttpHeaders.acceptHeader: acceptHeader}, + ).then((res) { + return res.body; + }).catchError((err) { + // 404 + return null; + }); + }; + }; + final readmeData = MarkdownViewData( + context, + md: readmeFactory('application/vnd.github.v3.raw'), + html: readmeFactory('application/vnd.github.v3.html'), + ); - return Tuple3(repo, countFuture, readmeFuture); + return Tuple3(repo, countFuture, readmeData); }, actionBuilder: (data, setState) { final repo = data.item1; @@ -97,7 +106,7 @@ class GhRepoScreen extends StatelessWidget { bodyBuilder: (data, setState) { final repo = data.item1; final contributionFuture = data.item2; - final readmeFuture = data.item3; + final readmeData = data.item3; final ref = branch == null ? repo.defaultBranchRef : repo.ref; final license = repo.licenseInfo?.spdxId ?? repo.licenseInfo?.name; @@ -332,16 +341,7 @@ class GhRepoScreen extends StatelessWidget { ], ], ), - FutureBuilder( - future: readmeFuture, - builder: (context, snapshot) { - if (snapshot.data == null) { - return Container(); - } else { - return MarkdownWebView(snapshot.data); - } - }, - ) + MarkdownView(readmeData), ], ); }, diff --git a/lib/screens/gl_project.dart b/lib/screens/gl_project.dart index f012578..5848eaf 100644 --- a/lib/screens/gl_project.dart +++ b/lib/screens/gl_project.dart @@ -24,7 +24,7 @@ class GlProjectScreen extends StatelessWidget { Widget build(BuildContext context) { return RefreshStatefulScaffold< Tuple4>, Future, - Future>>( + MarkdownViewData>>( title: AppBarTitle('Project'), fetch: () async { final auth = context.read(); @@ -41,25 +41,29 @@ class GlProjectScreen extends StatelessWidget { final memberCountFuture = auth .fetchGitlabWithPage('/projects/$id/members?per_page=1') .then((v) => v.total); - final readmeFuture = p.readmeUrl == null - ? Future.sync(() => null) - : auth - .fetchWithGitlabToken( - p.readmeUrl.replaceFirst(r'/blob/', '/raw/')) - .then((md) async { - // we should get the markdown content, then render it - // https://gitlab.com/gitlab-org/gitlab/-/issues/16335 - final res = await auth.fetchGitlab('/markdown', - isPost: true, - body: { - 'text': md, - 'gfm': true, - 'project': '${p.namespace.name}/${p.name}' - }); - return (res['html'] as String).normalizedHtml; - }); - return Tuple4(p, langFuture, memberCountFuture, readmeFuture); + MarkdownViewData readmeData; + if (p.readmeUrl != null) { + final md = () => auth.fetchWithGitlabToken( + p.readmeUrl.replaceFirst(r'/blob/', '/raw/')); + readmeData = MarkdownViewData( + context, + md: md, + html: () => md().then((md) async { + // we should get the markdown content, then render it + // https://gitlab.com/gitlab-org/gitlab/-/issues/16335 + final res = await auth.fetchGitlab('/markdown', + isPost: true, + body: { + 'text': md, + 'gfm': true, + 'project': '${p.namespace.name}/${p.name}' + }); + return (res['html'] as String).normalizedHtml; + }), + ); + } + return Tuple4(p, langFuture, memberCountFuture, readmeData); }, actionBuilder: (t, setState) { return ActionButton( @@ -73,7 +77,7 @@ class GlProjectScreen extends StatelessWidget { final p = t.item1; final langFuture = t.item2; final memberCountFuture = t.item3; - final readmeFuture = t.item4; + final readmeData = t.item4; final theme = Provider.of(context); final auth = Provider.of(context); @@ -185,16 +189,7 @@ class GlProjectScreen extends StatelessWidget { ], ), CommonStyle.verticalGap, - FutureBuilder( - future: readmeFuture, - builder: (context, snapshot) { - if (snapshot.data == null) { - return Container(); - } else { - return MarkdownWebView(snapshot.data); - } - }, - ), + MarkdownView(readmeData), ], ); }, diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index cd15ee9..f96a9e7 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -126,6 +126,28 @@ class SettingsScreen extends StatelessWidget { url: '/choose-code-theme', rightWidget: Text('${code.fontFamily}, ${code.fontSize}pt'), ), + TableViewItem( + text: Text('Markdown Render Engine'), + rightWidget: Text(theme.markdown == AppMarkdownType.flutter + ? 'Flutter' + : 'WebView'), + onTap: () { + theme.showActions(context, [ + for (var t in [ + Tuple2('Flutter', AppMarkdownType.flutter), + Tuple2('WebView', AppMarkdownType.webview), + ]) + ActionItem( + text: t.item1, + onTap: (_) { + if (theme.markdown != t.item2) { + theme.setMarkdown(t.item2); + } + }, + ) + ]); + }, + ), ]), CommonStyle.verticalGap, TableView(headerText: 'feedback', items: [ diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 406deff..5a32310 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -25,6 +25,7 @@ class StorageKeys { static const codeThemeDark = 'code-theme-dark'; static const iCodeFontSize = 'code-font-size'; static const codeFontFamily = 'code-font-family'; + static const markdown = 'markdown'; static getDefaultStartTabKey(String platform) => 'default-start-tab-$platform'; diff --git a/lib/widgets/markdown_view.dart b/lib/widgets/markdown_view.dart index d836846..de5a21c 100644 --- a/lib/widgets/markdown_view.dart +++ b/lib/widgets/markdown_view.dart @@ -8,6 +8,54 @@ import 'package:provider/provider.dart'; import 'package:uri/uri.dart'; import 'package:path/path.dart' as path; +class MarkdownViewData { + final Future future; + MarkdownViewData( + BuildContext context, { + @required Future Function() md, + @required Future Function() html, + }) : future = context.read().markdown == AppMarkdownType.flutter + ? md() + : html(); +} + +class MarkdownView extends StatelessWidget { + final MarkdownViewData data; + MarkdownView(this.data); + + @override + Widget build(BuildContext context) { + final theme = Provider.of(context); + + if (data?.future == null) return Container(); + + switch (theme.markdown) { + case AppMarkdownType.flutter: + return FutureBuilder( + future: data.future, + builder: (context, snapshot) { + if (snapshot.data == null) { + return Container(); + } else { + return MarkdownFlutterView(snapshot.data); + } + }, + ); + default: + return FutureBuilder( + future: data.future, + builder: (context, snapshot) { + if (snapshot.data == null) { + return Container(); + } else { + return MarkdownWebView(snapshot.data); + } + }, + ); + } + } +} + class MarkdownWebView extends StatelessWidget { final String html; MarkdownWebView(this.html);