Rename app branding to Pulseboard
bc6fa20 parent: e65e657 modified
app/lib/main.dart +6 -7 | @@ -5,17 +5,17 @@ import 'src/screens/home_shell.dart'; | ||
| 5 | 5 | import 'src/theme.dart'; |
| 6 | 6 | |
| 7 | 7 | void main() { |
| 8 | - runApp(const GleanApp()); | |
| 8 | + runApp(const PulseboardApp()); | |
| 9 | 9 | } |
| 10 | 10 | |
| 11 | -class GleanApp extends StatefulWidget { | |
| 12 | - const GleanApp({super.key}); | |
| 11 | +class PulseboardApp extends StatefulWidget { | |
| 12 | + const PulseboardApp({super.key}); | |
| 13 | 13 | |
| 14 | 14 | @override |
| 15 | - State<GleanApp> createState() => _GleanAppState(); | |
| 15 | + State<PulseboardApp> createState() => _PulseboardAppState(); | |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | -class _GleanAppState extends State<GleanApp> { | |
| 18 | +class _PulseboardAppState extends State<PulseboardApp> { | |
| 19 | 19 | final _state = AppState(); |
| 20 | 20 | |
| 21 | 21 | @override |
| @@ -36,7 +36,7 @@ class _GleanAppState extends State<GleanApp> { | ||
| 36 | 36 | return AppScope( |
| 37 | 37 | state: _state, |
| 38 | 38 | child: MaterialApp( |
| 39 | - title: 'glean', | |
| 39 | + title: 'Pulseboard', | |
| 40 | 40 | debugShowCheckedModeBanner: false, |
| 41 | 41 | theme: gleanTheme(Brightness.light), |
| 42 | 42 | darkTheme: gleanTheme(Brightness.dark), |
| @@ -46,4 +46,3 @@ class _GleanAppState extends State<GleanApp> { | ||
| 46 | 46 | } |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | - | |
| @@ -5,17 +5,17 @@ import 'src/screens/home_shell.dart'; | |||
| 5 | import 'src/theme.dart'; | 5 | import 'src/theme.dart'; |
| 6 | 6 | ||
| 7 | void main() { | 7 | void main() { |
| 8 | - runApp(const GleanApp()); | 8 | + runApp(const PulseboardApp()); |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | -class GleanApp extends StatefulWidget { | 11 | +class PulseboardApp extends StatefulWidget { |
| 12 | - const GleanApp({super.key}); | 12 | + const PulseboardApp({super.key}); |
| 13 | 13 | ||
| 14 | @override | 14 | @override |
| 15 | - State<GleanApp> createState() => _GleanAppState(); | 15 | + State<PulseboardApp> createState() => _PulseboardAppState(); |
| 16 | } | 16 | } |
| 17 | 17 | ||
| 18 | -class _GleanAppState extends State<GleanApp> { | 18 | +class _PulseboardAppState extends State<PulseboardApp> { |
| 19 | final _state = AppState(); | 19 | final _state = AppState(); |
| 20 | 20 | ||
| 21 | @override | 21 | @override |
| @@ -36,7 +36,7 @@ class _GleanAppState extends State<GleanApp> { | |||
| 36 | return AppScope( | 36 | return AppScope( |
| 37 | state: _state, | 37 | state: _state, |
| 38 | child: MaterialApp( | 38 | child: MaterialApp( |
| 39 | - title: 'glean', | 39 | + title: 'Pulseboard', |
| 40 | debugShowCheckedModeBanner: false, | 40 | debugShowCheckedModeBanner: false, |
| 41 | theme: gleanTheme(Brightness.light), | 41 | theme: gleanTheme(Brightness.light), |
| 42 | darkTheme: gleanTheme(Brightness.dark), | 42 | darkTheme: gleanTheme(Brightness.dark), |
| @@ -46,4 +46,3 @@ class _GleanAppState extends State<GleanApp> { | |||
| 46 | } | 46 | } |
| 47 | } | 47 | } |
| 48 | 48 | ||
| 49 | - | ||
deleted
app/lib/src/screens/article_screen.dart +0 -143 | deleted file mode 100644 | ||
| @@ -1,143 +0,0 @@ | ||
| 1 | -import 'package:flutter/material.dart'; | |
| 2 | -import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; | |
| 3 | -import 'package:url_launcher/url_launcher.dart'; | |
| 4 | - | |
| 5 | -import '../api/client.dart'; | |
| 6 | -import '../app_state.dart'; | |
| 7 | -import '../models/models.dart'; | |
| 8 | -import '../theme.dart'; | |
| 9 | -import '../widgets/common.dart'; | |
| 10 | - | |
| 11 | -/// Reader view for one article. | |
| 12 | -class ArticleScreen extends StatefulWidget { | |
| 13 | - const ArticleScreen({super.key, required this.articleId}); | |
| 14 | - | |
| 15 | - final int articleId; | |
| 16 | - | |
| 17 | - @override | |
| 18 | - State<ArticleScreen> createState() => _ArticleScreenState(); | |
| 19 | -} | |
| 20 | - | |
| 21 | -class _ArticleScreenState extends State<ArticleScreen> { | |
| 22 | - Future<Article>? _future; | |
| 23 | - Article? _article; | |
| 24 | - bool _fetching = false; | |
| 25 | - | |
| 26 | - @override | |
| 27 | - void initState() { | |
| 28 | - super.initState(); | |
| 29 | - _load(); | |
| 30 | - } | |
| 31 | - | |
| 32 | - void _load() { | |
| 33 | - setState(() { | |
| 34 | - _future = _fetch(); | |
| 35 | - }); | |
| 36 | - } | |
| 37 | - | |
| 38 | - Future<Article> _fetch() async { | |
| 39 | - final client = AppScope.read(context).client; | |
| 40 | - final a = await client.article(widget.articleId); | |
| 41 | - _article = a; | |
| 42 | - // Opening is what marks it read, as in the web interface. Best-effort: | |
| 43 | - // failing to record it must not break the reader. | |
| 44 | - if (!a.isRead) { | |
| 45 | - client.setRead(a.id).catchError((_) {}); | |
| 46 | - } | |
| 47 | - return a; | |
| 48 | - } | |
| 49 | - | |
| 50 | - Future<void> _fetchFullText() async { | |
| 51 | - setState(() => _fetching = true); | |
| 52 | - try { | |
| 53 | - final text = await AppScope.read(context).client | |
| 54 | - .fetchFullText(widget.articleId); | |
| 55 | - if (!mounted) return; | |
| 56 | - setState(() => _article = _article?.copyWith(content: text)); | |
| 57 | - } on ApiException catch (e) { | |
| 58 | - if (mounted) showToast(context, e.message); | |
| 59 | - } finally { | |
| 60 | - if (mounted) setState(() => _fetching = false); | |
| 61 | - } | |
| 62 | - } | |
| 63 | - | |
| 64 | - Future<void> _openOriginal() async { | |
| 65 | - final url = _article?.url ?? ''; | |
| 66 | - final uri = Uri.tryParse(url); | |
| 67 | - if (uri == null || url.isEmpty) return; | |
| 68 | - if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { | |
| 69 | - if (mounted) showToast(context, 'Could not open the link.'); | |
| 70 | - } | |
| 71 | - } | |
| 72 | - | |
| 73 | - @override | |
| 74 | - Widget build(BuildContext context) { | |
| 75 | - return Scaffold( | |
| 76 | - appBar: AppBar( | |
| 77 | - title: const Text('Read'), | |
| 78 | - actions: [ | |
| 79 | - IconButton( | |
| 80 | - tooltip: 'Open original', | |
| 81 | - icon: const Icon(Icons.open_in_new), | |
| 82 | - onPressed: _openOriginal, | |
| 83 | - ), | |
| 84 | - ], | |
| 85 | - ), | |
| 86 | - body: AsyncView<Article>( | |
| 87 | - future: _future, | |
| 88 | - onRetry: _load, | |
| 89 | - builder: (context, data) => _body(context, _article ?? data), | |
| 90 | - ), | |
| 91 | - ); | |
| 92 | - } | |
| 93 | - | |
| 94 | - Widget _body(BuildContext context, Article a) { | |
| 95 | - final text = Theme.of(context).textTheme; | |
| 96 | - final c = GleanColors.of(context); | |
| 97 | - | |
| 98 | - return ListView( | |
| 99 | - padding: const EdgeInsets.all(16), | |
| 100 | - children: [ | |
| 101 | - Row( | |
| 102 | - children: [ | |
| 103 | - Expanded(child: Text(a.feedTitle, style: text.bodySmall)), | |
| 104 | - Text(relativeTime(a.published), style: text.bodySmall), | |
| 105 | - ], | |
| 106 | - ), | |
| 107 | - const SizedBox(height: 12), | |
| 108 | - Text(a.title.isEmpty ? '(untitled)' : a.title, style: text.headlineSmall), | |
| 109 | - if (a.author.isNotEmpty) ...[ | |
| 110 | - const SizedBox(height: 6), | |
| 111 | - Text(a.author, style: text.bodySmall), | |
| 112 | - ], | |
| 113 | - const SizedBox(height: 16), | |
| 114 | - Divider(color: c.faint, height: 1), | |
| 115 | - const SizedBox(height: 16), | |
| 116 | - if (a.content.isEmpty) | |
| 117 | - Text('No content in the feed.', style: text.bodySmall) | |
| 118 | - else | |
| 119 | - // Rendering HTML from someone else's server is only safe because | |
| 120 | - // the reader sanitises every body it sends through the same | |
| 121 | - // whitelist its scraper uses. | |
| 122 | - HtmlWidget( | |
| 123 | - a.content, | |
| 124 | - textStyle: text.bodyLarge, | |
| 125 | - onTapUrl: (url) async { | |
| 126 | - final uri = Uri.tryParse(url); | |
| 127 | - if (uri != null) { | |
| 128 | - await launchUrl(uri, mode: LaunchMode.externalApplication); | |
| 129 | - } | |
| 130 | - return true; | |
| 131 | - }, | |
| 132 | - ), | |
| 133 | - const SizedBox(height: 24), | |
| 134 | - GleanButton( | |
| 135 | - label: 'Fetch full text', | |
| 136 | - busy: _fetching, | |
| 137 | - onPressed: _fetchFullText, | |
| 138 | - ), | |
| 139 | - const SizedBox(height: 40), | |
| 140 | - ], | |
| 141 | - ); | |
| 142 | - } | |
| 143 | -} | |
| deleted file mode 100644 | |||
| @@ -1,143 +0,0 @@ | |||
| 1 | -import 'package:flutter/material.dart'; | ||
| 2 | -import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; | ||
| 3 | -import 'package:url_launcher/url_launcher.dart'; | ||
| 4 | - | ||
| 5 | -import '../api/client.dart'; | ||
| 6 | -import '../app_state.dart'; | ||
| 7 | -import '../models/models.dart'; | ||
| 8 | -import '../theme.dart'; | ||
| 9 | -import '../widgets/common.dart'; | ||
| 10 | - | ||
| 11 | -/// Reader view for one article. | ||
| 12 | -class ArticleScreen extends StatefulWidget { | ||
| 13 | - const ArticleScreen({super.key, required this.articleId}); | ||
| 14 | - | ||
| 15 | - final int articleId; | ||
| 16 | - | ||
| 17 | - @override | ||
| 18 | - State<ArticleScreen> createState() => _ArticleScreenState(); | ||
| 19 | -} | ||
| 20 | - | ||
| 21 | -class _ArticleScreenState extends State<ArticleScreen> { | ||
| 22 | - Future<Article>? _future; | ||
| 23 | - Article? _article; | ||
| 24 | - bool _fetching = false; | ||
| 25 | - | ||
| 26 | - @override | ||
| 27 | - void initState() { | ||
| 28 | - super.initState(); | ||
| 29 | - _load(); | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | - void _load() { | ||
| 33 | - setState(() { | ||
| 34 | - _future = _fetch(); | ||
| 35 | - }); | ||
| 36 | - } | ||
| 37 | - | ||
| 38 | - Future<Article> _fetch() async { | ||
| 39 | - final client = AppScope.read(context).client; | ||
| 40 | - final a = await client.article(widget.articleId); | ||
| 41 | - _article = a; | ||
| 42 | - // Opening is what marks it read, as in the web interface. Best-effort: | ||
| 43 | - // failing to record it must not break the reader. | ||
| 44 | - if (!a.isRead) { | ||
| 45 | - client.setRead(a.id).catchError((_) {}); | ||
| 46 | - } | ||
| 47 | - return a; | ||
| 48 | - } | ||
| 49 | - | ||
| 50 | - Future<void> _fetchFullText() async { | ||
| 51 | - setState(() => _fetching = true); | ||
| 52 | - try { | ||
| 53 | - final text = await AppScope.read(context).client | ||
| 54 | - .fetchFullText(widget.articleId); | ||
| 55 | - if (!mounted) return; | ||
| 56 | - setState(() => _article = _article?.copyWith(content: text)); | ||
| 57 | - } on ApiException catch (e) { | ||
| 58 | - if (mounted) showToast(context, e.message); | ||
| 59 | - } finally { | ||
| 60 | - if (mounted) setState(() => _fetching = false); | ||
| 61 | - } | ||
| 62 | - } | ||
| 63 | - | ||
| 64 | - Future<void> _openOriginal() async { | ||
| 65 | - final url = _article?.url ?? ''; | ||
| 66 | - final uri = Uri.tryParse(url); | ||
| 67 | - if (uri == null || url.isEmpty) return; | ||
| 68 | - if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { | ||
| 69 | - if (mounted) showToast(context, 'Could not open the link.'); | ||
| 70 | - } | ||
| 71 | - } | ||
| 72 | - | ||
| 73 | - @override | ||
| 74 | - Widget build(BuildContext context) { | ||
| 75 | - return Scaffold( | ||
| 76 | - appBar: AppBar( | ||
| 77 | - title: const Text('Read'), | ||
| 78 | - actions: [ | ||
| 79 | - IconButton( | ||
| 80 | - tooltip: 'Open original', | ||
| 81 | - icon: const Icon(Icons.open_in_new), | ||
| 82 | - onPressed: _openOriginal, | ||
| 83 | - ), | ||
| 84 | - ], | ||
| 85 | - ), | ||
| 86 | - body: AsyncView<Article>( | ||
| 87 | - future: _future, | ||
| 88 | - onRetry: _load, | ||
| 89 | - builder: (context, data) => _body(context, _article ?? data), | ||
| 90 | - ), | ||
| 91 | - ); | ||
| 92 | - } | ||
| 93 | - | ||
| 94 | - Widget _body(BuildContext context, Article a) { | ||
| 95 | - final text = Theme.of(context).textTheme; | ||
| 96 | - final c = GleanColors.of(context); | ||
| 97 | - | ||
| 98 | - return ListView( | ||
| 99 | - padding: const EdgeInsets.all(16), | ||
| 100 | - children: [ | ||
| 101 | - Row( | ||
| 102 | - children: [ | ||
| 103 | - Expanded(child: Text(a.feedTitle, style: text.bodySmall)), | ||
| 104 | - Text(relativeTime(a.published), style: text.bodySmall), | ||
| 105 | - ], | ||
| 106 | - ), | ||
| 107 | - const SizedBox(height: 12), | ||
| 108 | - Text(a.title.isEmpty ? '(untitled)' : a.title, style: text.headlineSmall), | ||
| 109 | - if (a.author.isNotEmpty) ...[ | ||
| 110 | - const SizedBox(height: 6), | ||
| 111 | - Text(a.author, style: text.bodySmall), | ||
| 112 | - ], | ||
| 113 | - const SizedBox(height: 16), | ||
| 114 | - Divider(color: c.faint, height: 1), | ||
| 115 | - const SizedBox(height: 16), | ||
| 116 | - if (a.content.isEmpty) | ||
| 117 | - Text('No content in the feed.', style: text.bodySmall) | ||
| 118 | - else | ||
| 119 | - // Rendering HTML from someone else's server is only safe because | ||
| 120 | - // the reader sanitises every body it sends through the same | ||
| 121 | - // whitelist its scraper uses. | ||
| 122 | - HtmlWidget( | ||
| 123 | - a.content, | ||
| 124 | - textStyle: text.bodyLarge, | ||
| 125 | - onTapUrl: (url) async { | ||
| 126 | - final uri = Uri.tryParse(url); | ||
| 127 | - if (uri != null) { | ||
| 128 | - await launchUrl(uri, mode: LaunchMode.externalApplication); | ||
| 129 | - } | ||
| 130 | - return true; | ||
| 131 | - }, | ||
| 132 | - ), | ||
| 133 | - const SizedBox(height: 24), | ||
| 134 | - GleanButton( | ||
| 135 | - label: 'Fetch full text', | ||
| 136 | - busy: _fetching, | ||
| 137 | - onPressed: _fetchFullText, | ||
| 138 | - ), | ||
| 139 | - const SizedBox(height: 40), | ||
| 140 | - ], | ||
| 141 | - ); | ||
| 142 | - } | ||
| 143 | -} | ||
modified
app/web/index.html +3 -3 | @@ -18,18 +18,18 @@ | ||
| 18 | 18 | |
| 19 | 19 | <meta charset="UTF-8"> |
| 20 | 20 | <meta content="IE=Edge" http-equiv="X-UA-Compatible"> |
| 21 | - <meta name="description" content="A new Flutter project."> | |
| 21 | + <meta name="description" content="A personal feed reader."> | |
| 22 | 22 | |
| 23 | 23 | <!-- iOS meta tags & icons --> |
| 24 | 24 | <meta name="mobile-web-app-capable" content="yes"> |
| 25 | 25 | <meta name="apple-mobile-web-app-status-bar-style" content="black"> |
| 26 | - <meta name="apple-mobile-web-app-title" content="glean_app"> | |
| 26 | + <meta name="apple-mobile-web-app-title" content="Pulseboard"> | |
| 27 | 27 | <link rel="apple-touch-icon" href="icons/Icon-192.png"> |
| 28 | 28 | |
| 29 | 29 | <!-- Favicon --> |
| 30 | 30 | <link rel="icon" type="image/png" href="favicon.png"/> |
| 31 | 31 | |
| 32 | - <title>glean_app</title> | |
| 32 | + <title>Pulseboard</title> | |
| 33 | 33 | <link rel="manifest" href="manifest.json"> |
| 34 | 34 | </head> |
| 35 | 35 | <body> |
| @@ -18,18 +18,18 @@ | |||
| 18 | 18 | ||
| 19 | <meta charset="UTF-8"> | 19 | <meta charset="UTF-8"> |
| 20 | <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | 20 | <meta content="IE=Edge" http-equiv="X-UA-Compatible"> |
| 21 | - <meta name="description" content="A new Flutter project."> | 21 | + <meta name="description" content="A personal feed reader."> |
| 22 | 22 | ||
| 23 | <!-- iOS meta tags & icons --> | 23 | <!-- iOS meta tags & icons --> |
| 24 | <meta name="mobile-web-app-capable" content="yes"> | 24 | <meta name="mobile-web-app-capable" content="yes"> |
| 25 | <meta name="apple-mobile-web-app-status-bar-style" content="black"> | 25 | <meta name="apple-mobile-web-app-status-bar-style" content="black"> |
| 26 | - <meta name="apple-mobile-web-app-title" content="glean_app"> | 26 | + <meta name="apple-mobile-web-app-title" content="Pulseboard"> |
| 27 | <link rel="apple-touch-icon" href="icons/Icon-192.png"> | 27 | <link rel="apple-touch-icon" href="icons/Icon-192.png"> |
| 28 | 28 | ||
| 29 | <!-- Favicon --> | 29 | <!-- Favicon --> |
| 30 | <link rel="icon" type="image/png" href="favicon.png"/> | 30 | <link rel="icon" type="image/png" href="favicon.png"/> |
| 31 | 31 | ||
| 32 | - <title>glean_app</title> | 32 | + <title>Pulseboard</title> |
| 33 | <link rel="manifest" href="manifest.json"> | 33 | <link rel="manifest" href="manifest.json"> |
| 34 | </head> | 34 | </head> |
| 35 | <body> | 35 | <body> |
modified
app/web/manifest.json +3 -3 | @@ -1,11 +1,11 @@ | ||
| 1 | 1 | { |
| 2 | - "name": "glean_app", | |
| 3 | - "short_name": "glean_app", | |
| 2 | + "name": "Pulseboard", | |
| 3 | + "short_name": "Pulseboard", | |
| 4 | 4 | "start_url": ".", |
| 5 | 5 | "display": "standalone", |
| 6 | 6 | "background_color": "#0175C2", |
| 7 | 7 | "theme_color": "#0175C2", |
| 8 | - "description": "A new Flutter project.", | |
| 8 | + "description": "A personal feed reader.", | |
| 9 | 9 | "orientation": "portrait-primary", |
| 10 | 10 | "prefer_related_applications": false, |
| 11 | 11 | "icons": [ |
| @@ -1,11 +1,11 @@ | |||
| 1 | { | 1 | { |
| 2 | - "name": "glean_app", | 2 | + "name": "Pulseboard", |
| 3 | - "short_name": "glean_app", | 3 | + "short_name": "Pulseboard", |
| 4 | "start_url": ".", | 4 | "start_url": ".", |
| 5 | "display": "standalone", | 5 | "display": "standalone", |
| 6 | "background_color": "#0175C2", | 6 | "background_color": "#0175C2", |
| 7 | "theme_color": "#0175C2", | 7 | "theme_color": "#0175C2", |
| 8 | - "description": "A new Flutter project.", | 8 | + "description": "A personal feed reader.", |
| 9 | "orientation": "portrait-primary", | 9 | "orientation": "portrait-primary", |
| 10 | "prefer_related_applications": false, | 10 | "prefer_related_applications": false, |
| 11 | "icons": [ | 11 | "icons": [ |