feat: add markdown render engine setting

closes #124
This commit is contained in:
Rongjian Zhang 2020-11-06 18:47:56 +08:00
parent 9db5a201a3
commit 5795413f25
6 changed files with 158 additions and 57 deletions

View File

@ -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<T> {
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<void> 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<String> getMarkdownFuture(
BuildContext context, {
@required Future<String> Function() md,
@required Future<String> 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<void> 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();
}

View File

@ -53,7 +53,7 @@ class GhRepoScreen extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Provider.of<ThemeModel>(context);
return RefreshStatefulScaffold<
Tuple3<GhRepoRepository, Future<int>, Future<String>>>(
Tuple3<GhRepoRepository, Future<int>, MarkdownViewData>>(
title: AppBarTitle('Repository'),
fetch: () async {
final ghClient = context.read<AuthModel>().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<String>(
future: readmeFuture,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Container();
} else {
return MarkdownWebView(snapshot.data);
}
},
)
MarkdownView(readmeData),
],
);
},

View File

@ -24,7 +24,7 @@ class GlProjectScreen extends StatelessWidget {
Widget build(BuildContext context) {
return RefreshStatefulScaffold<
Tuple4<GitlabProject, Future<Map<String, double>>, Future<int>,
Future<String>>>(
MarkdownViewData>>(
title: AppBarTitle('Project'),
fetch: () async {
final auth = context.read<AuthModel>();
@ -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<ThemeModel>(context);
final auth = Provider.of<AuthModel>(context);
@ -185,16 +189,7 @@ class GlProjectScreen extends StatelessWidget {
],
),
CommonStyle.verticalGap,
FutureBuilder<String>(
future: readmeFuture,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Container();
} else {
return MarkdownWebView(snapshot.data);
}
},
),
MarkdownView(readmeData),
],
);
},

View File

@ -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: [

View File

@ -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';

View File

@ -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<String> future;
MarkdownViewData(
BuildContext context, {
@required Future<String> Function() md,
@required Future<String> Function() html,
}) : future = context.read<ThemeModel>().markdown == AppMarkdownType.flutter
? md()
: html();
}
class MarkdownView extends StatelessWidget {
final MarkdownViewData data;
MarkdownView(this.data);
@override
Widget build(BuildContext context) {
final theme = Provider.of<ThemeModel>(context);
if (data?.future == null) return Container();
switch (theme.markdown) {
case AppMarkdownType.flutter:
return FutureBuilder<String>(
future: data.future,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Container();
} else {
return MarkdownFlutterView(snapshot.data);
}
},
);
default:
return FutureBuilder<String>(
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);