Add the Fun section to the Learn tab with comic-friendly reader view (#3)
The Learn tab gets a Fun section below Learning with the last 3 days of Fun articles. Fun articles stay out of Today and the Briefing. New curated Fun sources: xkcd and The Daily WTF (on by default) and r/ProgrammerHumor top of the day (off). Reader view for Fun articles shows the picture whole in the body instead of as a 16:9 banner cropped to fit, and doesn't call a comic without text a teaser. When the page gives no picture (Reddit blocks reader view), the feed's image is shown. The extractor now keeps an image's title text as a caption (xkcd's last joke), images in blockquotes and behind in-page anchors (The Daily WTF's Error'd screenshots), drops image maps and license boxes, and on a text tie prefers the page with more blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a6b8d8b537
commit
8240a767ef
10 files changed
+387
-20
No files matched your search
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
import '../models/article.dart';
|
||||
import '../models/news_source.dart';
|
||||
import '../services/article_extractor.dart';
|
||||
import '../services/consent_service.dart';
|
||||
import '../services/content_blocking.dart';
|
||||
@@ -275,12 +276,22 @@ class _ReaderView extends StatelessWidget {
|
||||
builder: (context, snapshot) {
|
||||
final loaded = snapshot.data;
|
||||
var blocks = loaded?.blocks ?? const <ReaderBlock>[];
|
||||
// Comics and memes are the picture: show it whole in the body
|
||||
// rather than as a cropped banner.
|
||||
final pictureLed = article.source.kind == SourceKind.fun;
|
||||
final banner = pictureLed ? null : article.imageUrl;
|
||||
// The page's own hero image duplicates the feed image shown on top.
|
||||
if (article.imageUrl != null) {
|
||||
if (banner != null) {
|
||||
blocks = blocks.skipWhile((b) => b.type == BlockType.image).toList();
|
||||
}
|
||||
if (pictureLed &&
|
||||
article.imageUrl != null &&
|
||||
!blocks.any((b) => b.type == BlockType.image)) {
|
||||
blocks = [ReaderBlock(BlockType.image, article.imageUrl!), ...blocks];
|
||||
}
|
||||
final isFull = loaded != null &&
|
||||
loaded.plainText.length >= max(400, article.excerpt.length * 2);
|
||||
(loaded.plainText.length >= max(400, article.excerpt.length * 2) ||
|
||||
(pictureLed && blocks.any((b) => b.type == BlockType.image)));
|
||||
// Skip the lead if the body already starts with it.
|
||||
final firstText = blocks.where((b) => b.type == BlockType.paragraph).firstOrNull;
|
||||
final showLead = article.excerpt.isNotEmpty &&
|
||||
@@ -291,9 +302,9 @@ class _ReaderView extends StatelessWidget {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.only(bottom: 48),
|
||||
children: [
|
||||
if (article.imageUrl != null)
|
||||
if (banner != null)
|
||||
Image.network(
|
||||
article.imageUrl!,
|
||||
banner,
|
||||
fit: BoxFit.cover,
|
||||
webHtmlElementStrategy: WebHtmlElementStrategy.prefer,
|
||||
// Take up space only once the image has loaded, so a broken
|
||||
@@ -336,7 +347,7 @@ class _ReaderView extends StatelessWidget {
|
||||
),
|
||||
if (isFull)
|
||||
for (final block in blocks)
|
||||
if (block.text != article.imageUrl) _BlockView(block: block),
|
||||
if (block.text != banner) _BlockView(block: block),
|
||||
if (loaded != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
const Divider(),
|
||||
|
||||
@@ -13,7 +13,7 @@ class LearnScreen extends StatelessWidget {
|
||||
final AppState state;
|
||||
|
||||
/// The sections, top to bottom.
|
||||
static const kinds = [SourceKind.learning];
|
||||
static const kinds = [SourceKind.learning, SourceKind.fun];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in new issue
Block a user