Skip to content

Commit

Permalink
feat: real time settings update
Browse files Browse the repository at this point in the history
Parsed article is now a class,
so that parts can be shown/hidden without re-downloading it.
This paves the way to templates.
  • Loading branch information
sanzoghenzo committed Nov 9, 2023
1 parent 235d34d commit 9018bc7
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 108 deletions.
87 changes: 51 additions & 36 deletions lib/converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,77 @@ import 'package:html2md/html2md.dart' as html2md;
import 'package:intl/intl.dart';
import 'package:markdownr/notifications.dart';
import 'package:markdownr/readability.dart';
import 'package:markdownr/settings.dart';
import 'package:markdownr/httpclient.dart';

class MarkdownArticle {
const MarkdownArticle({
required this.url,
required this.content,
required this.title,
required this.author,
required this.excerpt,
required this.creationDate,
});

final String url;
final String content;
final String title;
final String author;
final String excerpt;
final String creationDate;

String get frontMatter =>
"---\ncreated: $creationDate\nsource: $url\nauthor: $author\n---\n\n";

String get sourceLinkSection => "Clipped from: $url\n\n";

String get excerptSection => "## Excerpt\n\n> $excerpt\n\n";
}

class Url2MdConverter {
Url2MdConverter({
required HttpClient httpClient,
required SettingsRepository settingsRepository,
required NotificationService notificationService,
required ReadabilityService readabilityService,
}) : _httpClient = httpClient,
_settingsRepository = settingsRepository,
_notificationService = notificationService,
_readabilityService = readabilityService;

final HttpClient _httpClient;
final SettingsRepository _settingsRepository;
final NotificationService _notificationService;
final ReadabilityService _readabilityService;

Future<String> convert({required String url}) async {
Future<MarkdownArticle> convertPage({required String url}) async {
var dateFmt = DateFormat("yyyy-MM-ddThh:mm:ss");
var formattedDate = dateFmt.format(DateTime.now());
try {
var html = await _httpClient.getPage(url);
var readableResults = await _readabilityService.makeReadable(html, url);
var markdown =
_settingsRepository.getBool("includeBody", defaultValue: true)
? html2md.convert(readableResults.content, styleOptions: {
"headingStyle": "atx",
"hr": "---",
"bulletListMarker": "-",
"codeBlockStyle": "fenced",
})
: "";
var preamble = await _buildPreamble(readableResults, url);
return "$preamble$markdown";
var markdown = html2md.convert(readableResults.content, styleOptions: {
"headingStyle": "atx",
"hr": "---",
"bulletListMarker": "-",
"codeBlockStyle": "fenced",
});
var title = readableResults.title;
var author = readableResults.author;
var excerpt = readableResults.excerpt;
return MarkdownArticle(
url: url,
content: markdown,
title: title,
author: author,
excerpt: excerpt,
creationDate: formattedDate);
} catch (e) {
_notificationService.showToast("$e");
return "";
return MarkdownArticle(
url: url,
content: "",
title: "",
author: "",
excerpt: "",
creationDate: formattedDate);
}
}

Future<String> _buildPreamble(
ReadabilityOutput readableResults, String url) async {
var title = readableResults.title;
var author = readableResults.author;
var excerpt = readableResults.excerpt;
var dateFmt = DateFormat("yyyy-MM-ddThh:mm:ss");
var formattedDate = dateFmt.format(DateTime.now());
var frontMatter = _settingsRepository.getBool("includeFrontMatter")
? "---\ncreated: $formattedDate\nsource: $url\nauthor: $author\n---\n\n"
: "";
var link = _settingsRepository.getBool("includeSourceLink")
? "Clipped from: $url\n\n"
: "";
var excerptString = _settingsRepository.getBool("includeExcerpt")
? "## Excerpt\n\n> $excerpt\n\n"
: "";
return "$frontMatter# $title\n\n$link$excerptString";
}
}
112 changes: 61 additions & 51 deletions lib/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class _HomePageState extends State<HomePage> {
bool includeExcerpt = false;
bool includeBody = true;
bool showPreview = false;
MarkdownArticle? article;
late final Url2MdConverter? _url2MdConverter;
late final SettingsRepository _settingsRepository;

Expand All @@ -42,8 +43,8 @@ class _HomePageState extends State<HomePage> {
// share intent received while running
_intentDataStreamSubscription =
ReceiveSharingIntent.getTextStream().listen(fromIntent, onError: (err) {
Fluttertoast.showToast(msg: "Error receiving the intent: $err");
});
Fluttertoast.showToast(msg: "Error receiving the intent: $err");
});

// share intent received while closed
ReceiveSharingIntent.getInitialText().then((String? value) async {
Expand All @@ -61,7 +62,6 @@ class _HomePageState extends State<HomePage> {
_settingsRepository = repo;
_url2MdConverter = Url2MdConverter(
httpClient: const DefaultHttpClient(),
settingsRepository: _settingsRepository,
notificationService: const DefaultNotificationService(),
readabilityService: DefaultReadabilityService());
includeFrontMatter = _settingsRepository.getBool("includeFrontMatter");
Expand All @@ -84,11 +84,11 @@ class _HomePageState extends State<HomePage> {
var repo = await repoFactory();
initStateInternal(repo);
}
var md = await _url2MdConverter?.convert(url: value);
setState(() {
url = value;
markdown = md!;
});
article = await _url2MdConverter?.convertPage(url: value);
_updateMarkdown();
if (markdown.isNotEmpty) {
Share.share(markdown);
}
Expand Down Expand Up @@ -158,63 +158,63 @@ class _HomePageState extends State<HomePage> {

Padding _buildUrlInput() {
return Padding(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
hintText: 'Enter a URL',
),
onChanged: (value) {
url = value;
},
controller: _controller,
),
);
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
hintText: 'Enter a URL',
),
onChanged: (value) {
url = value;
},
controller: _controller,
),
);
}

Expanded _buildMarkdownView() {
return Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
scrollDirection: Axis.vertical, //.horizontal
child: showPreview
? MarkdownBody(data: markdown)
: Text(markdown, softWrap: true),
),
),
);
child: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
scrollDirection: Axis.vertical, //.horizontal
child: showPreview
? MarkdownBody(data: markdown)
: Text(markdown, softWrap: true),
),
),
);
}

Row _buildButtonsRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
children: <Widget>[
Column(
children: <Widget>[
ElevatedButton(
onPressed: _convert,
child: const Text('CONVERT'),
),
],
ElevatedButton(
onPressed: _convert,
child: const Text('CONVERT'),
),
Column(
children: <Widget>[
ElevatedButton(
onPressed: markdown.isNotEmpty ? _share : null,
child: const Text('SHARE'),
),
],
],
),
Column(
children: <Widget>[
ElevatedButton(
onPressed: markdown.isNotEmpty ? _share : null,
child: const Text('SHARE'),
),
Column(
children: <Widget>[
ElevatedButton(
onPressed: markdown.isNotEmpty ? _toClipboard : null,
child: const Text('COPY'),
),
],
],
),
Column(
children: <Widget>[
ElevatedButton(
onPressed: markdown.isNotEmpty ? _toClipboard : null,
child: const Text('COPY'),
),
],
);
),
],
);
}

void _handleSettings(int result) {
Expand All @@ -237,6 +237,7 @@ class _HomePageState extends State<HomePage> {
break;
}
});
_updateMarkdown();
}

bool _togglePreference(String settingName) {
Expand All @@ -246,9 +247,18 @@ class _HomePageState extends State<HomePage> {
}

void _convert() async {
var md = await _url2MdConverter?.convert(url: url);
article = await _url2MdConverter?.convertPage(url: url);
_updateMarkdown();
}

void _updateMarkdown() {
var title = article!.title;
var body = includeBody ? article!.content : "";
var frontMatter = includeFrontMatter ? article!.frontMatter : "";
var link = includeSourceLink ? article!.sourceLinkSection : "";
var excerpt = includeExcerpt ? article!.excerptSection : "";
setState(() {
markdown = md!;
markdown = "$frontMatter# $title\n\n$link$excerpt$body";
});
}

Expand Down
35 changes: 14 additions & 21 deletions test/converter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import 'package:markdownr/converter.dart';
import 'package:markdownr/httpclient.dart';
import 'package:markdownr/notifications.dart';
import 'package:markdownr/readability.dart';
import 'package:markdownr/settings.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

@GenerateNiceMocks([
MockSpec<HttpClient>(),
MockSpec<SettingsRepository>(),
MockSpec<NotificationService>(),
MockSpec<ReadabilityService>(),
])
Expand All @@ -18,19 +16,16 @@ import 'converter_test.mocks.dart';
void main() {
group('Url2MdConverter', () {
late MockHttpClient mockHttpClient;
late MockSettingsRepository mockSettingsRepository;
late MockNotificationService mockNotificationService;
late Url2MdConverter url2mdConverter;
late ReadabilityService mockReadabilityService;

setUp(() {
mockHttpClient = MockHttpClient();
mockSettingsRepository = MockSettingsRepository();
mockNotificationService = MockNotificationService();
mockReadabilityService = MockReadabilityService();
url2mdConverter = Url2MdConverter(
httpClient: mockHttpClient,
settingsRepository: mockSettingsRepository,
notificationService: mockNotificationService,
readabilityService: mockReadabilityService,
);
Expand All @@ -41,32 +36,30 @@ void main() {
const content = '<h2>Test Header</h2><p>Test paragraph</p>';
const html =
'<html><head><title>Test Title</title></head><body>$content</body></html>';
const expectedMarkdown =
'# Test Title\n\n## Test Header\n\nTest paragraph';
when(mockHttpClient.getPage(url)).thenAnswer((_) async => html);
when(mockSettingsRepository.getBool('includeFrontMatter'))
.thenAnswer((_) => false);
when(mockSettingsRepository.getBool('includeSourceLink'))
.thenAnswer((_) => false);
when(mockSettingsRepository.getBool('includeExcerpt'))
.thenAnswer((_) => false);
when(mockSettingsRepository.getBool('includeBody', defaultValue: true))
.thenAnswer((_) => true);
when(mockReadabilityService.makeReadable(html, url)).thenAnswer(
(_) async => const ReadabilityOutput(
content: '<div>$content</div>',
title: "Test Title",
author: "",
excerpt: ""));
final markdown = await url2mdConverter.convert(url: url);
expect(markdown, expectedMarkdown);
author: "Myself",
excerpt: "A really good article"));
final markdown = await url2mdConverter.convertPage(url: url);
expect(markdown.url, url);
expect(markdown.content, "## Test Header\n\nTest paragraph");
expect(markdown.title, "Test Title");
expect(markdown.author, "Myself");
expect(markdown.excerpt, "A really good article");
});

test('convert should handle exceptions', () async {
const url = 'https://example.com';
when(mockHttpClient.getPage(url)).thenThrow(Exception());
final markdown = await url2mdConverter.convert(url: url);
expect(markdown, '');
final markdown = await url2mdConverter.convertPage(url: url);
expect(markdown.url, url);
expect(markdown.content, "");
expect(markdown.title, "");
expect(markdown.author, "");
expect(markdown.excerpt, "");
});
});
}

0 comments on commit 9018bc7

Please sign in to comment.