nandi/gleanpublic Fork
⑂ bc6fa20
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Rename app branding to Pulseboard

nandithebull committed 2026-09-21T22:36:26-07:00 Browse files
bc6fa20 parent: e65e657
modified app/lib/main.dart +6 -7
@@ -5,17 +5,17 @@ import 'src/screens/home_shell.dart';
55 import 'src/theme.dart';
66
77 void main() {
8- runApp(const GleanApp());
8+ runApp(const PulseboardApp());
99 }
1010
11-class GleanApp extends StatefulWidget {
12- const GleanApp({super.key});
11+class PulseboardApp extends StatefulWidget {
12+ const PulseboardApp({super.key});
1313
1414 @override
15- State<GleanApp> createState() => _GleanAppState();
15+ State<PulseboardApp> createState() => _PulseboardAppState();
1616 }
1717
18-class _GleanAppState extends State<GleanApp> {
18+class _PulseboardAppState extends State<PulseboardApp> {
1919 final _state = AppState();
2020
2121 @override
@@ -36,7 +36,7 @@ class _GleanAppState extends State<GleanApp> {
3636 return AppScope(
3737 state: _state,
3838 child: MaterialApp(
39- title: 'glean',
39+ title: 'Pulseboard',
4040 debugShowCheckedModeBanner: false,
4141 theme: gleanTheme(Brightness.light),
4242 darkTheme: gleanTheme(Brightness.dark),
@@ -46,4 +46,3 @@ class _GleanAppState extends State<GleanApp> {
4646 }
4747 }
4848
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 @override14 @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 @override21 @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 @@
1818
1919 <meta charset="UTF-8">
2020 <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.">
2222
2323 <!-- iOS meta tags & icons -->
2424 <meta name="mobile-web-app-capable" content="yes">
2525 <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">
2727 <link rel="apple-touch-icon" href="icons/Icon-192.png">
2828
2929 <!-- Favicon -->
3030 <link rel="icon" type="image/png" href="favicon.png"/>
3131
32- <title>glean_app</title>
32+ <title>Pulseboard</title>
3333 <link rel="manifest" href="manifest.json">
3434 </head>
3535 <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 @@
11 {
2- "name": "glean_app",
3- "short_name": "glean_app",
2+ "name": "Pulseboard",
3+ "short_name": "Pulseboard",
44 "start_url": ".",
55 "display": "standalone",
66 "background_color": "#0175C2",
77 "theme_color": "#0175C2",
8- "description": "A new Flutter project.",
8+ "description": "A personal feed reader.",
99 "orientation": "portrait-primary",
1010 "prefer_related_applications": false,
1111 "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": [