Skip to content

Commit af26037

Browse files
committed
feat: enable Linux download and add direct binary downloads on website
- Remove beta/coming soon state from Linux download card - Fetch latest release assets from GitHub API to get direct download URLs instead of redirecting to the GitHub releases page - Show latest version tag below download cards - Add http package for API calls
1 parent e1709c2 commit af26037

File tree

3 files changed

+115
-99
lines changed

3 files changed

+115
-99
lines changed

website/lib/sections/download_section.dart

Lines changed: 113 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,78 @@
1+
import 'dart:convert';
12
import 'package:flutter/material.dart';
23
import 'package:google_fonts/google_fonts.dart';
4+
import 'package:http/http.dart' as http;
35
import 'package:url_launcher/url_launcher.dart';
46
import '../theme/web_theme.dart';
57
import '../widgets/responsive.dart';
68
import '../widgets/animated_on_scroll.dart';
79

8-
class DownloadSection extends StatelessWidget {
10+
class DownloadSection extends StatefulWidget {
911
final bool isDark;
1012

1113
const DownloadSection({super.key, required this.isDark});
1214

15+
@override
16+
State<DownloadSection> createState() => _DownloadSectionState();
17+
}
18+
19+
class _DownloadSectionState extends State<DownloadSection> {
20+
String? _macUrl;
21+
String? _linuxUrl;
22+
String? _version;
23+
bool _loading = true;
24+
25+
@override
26+
void initState() {
27+
super.initState();
28+
_fetchReleaseAssets();
29+
}
30+
31+
Future<void> _fetchReleaseAssets() async {
32+
try {
33+
final response = await http.get(
34+
Uri.parse('https://api.github.com/repos/maskedsyntax/patterns/releases/latest'),
35+
headers: {'Accept': 'application/vnd.github+json'},
36+
);
37+
if (response.statusCode == 200) {
38+
final data = jsonDecode(response.body);
39+
final tagName = data['tag_name'] as String?;
40+
final assets = data['assets'] as List;
41+
for (final asset in assets) {
42+
final name = asset['name'] as String;
43+
final url = asset['browser_download_url'] as String;
44+
if (name.endsWith('.dmg')) {
45+
_macUrl = url;
46+
} else if (name.endsWith('.deb')) {
47+
_linuxUrl = url;
48+
}
49+
}
50+
_version = tagName;
51+
}
52+
} catch (_) {
53+
// Fallback to releases page if API fails
54+
}
55+
if (mounted) setState(() => _loading = false);
56+
}
57+
58+
void _download(String? assetUrl) {
59+
final url = assetUrl ?? 'https://github.com/maskedsyntax/patterns/releases/latest';
60+
launchUrl(Uri.parse(url));
61+
}
62+
1363
@override
1464
Widget build(BuildContext context) {
1565
final screen = Responsive.getScreenSize(context);
1666
final isMobile = screen == ScreenSize.mobile;
17-
final textColor = isDark ? WebTheme.darkText : WebTheme.lightText;
67+
final textColor = widget.isDark ? WebTheme.darkText : WebTheme.lightText;
1868
final secondaryText =
19-
isDark ? WebTheme.darkTextSecondary : WebTheme.lightTextSecondary;
20-
final accent = isDark ? WebTheme.primaryYellow : WebTheme.primaryGold;
21-
final border = isDark ? WebTheme.darkBorder : WebTheme.lightBorder;
69+
widget.isDark ? WebTheme.darkTextSecondary : WebTheme.lightTextSecondary;
70+
final accent = widget.isDark ? WebTheme.primaryYellow : WebTheme.primaryGold;
71+
final border = widget.isDark ? WebTheme.darkBorder : WebTheme.lightBorder;
2272

2373
return Container(
2474
width: double.infinity,
25-
color: isDark ? WebTheme.darkBg : WebTheme.lightBg,
75+
color: widget.isDark ? WebTheme.darkBg : WebTheme.lightBg,
2676
child: ContentContainer(
2777
padding: Responsive.sectionPadding(context),
2878
child: AnimatedOnScroll(
@@ -62,27 +112,26 @@ class DownloadSection extends StatelessWidget {
62112
icon: Icons.desktop_mac_rounded,
63113
format: '.dmg',
64114
description: 'macOS 12 Monterey or later',
65-
url:
66-
'https://github.com/maskedsyntax/patterns/releases/latest',
67-
isDark: isDark,
115+
onDownload: () => _download(_macUrl),
116+
isDark: widget.isDark,
68117
accent: accent,
69118
textColor: textColor,
70119
secondaryText: secondaryText,
71120
border: border,
121+
loading: _loading,
72122
),
73123
_DownloadCard(
74124
platform: 'Linux',
75125
icon: Icons.terminal_rounded,
76126
format: '.deb',
77127
description: 'Ubuntu, Debian, and derivatives',
78-
url:
79-
'https://github.com/maskedsyntax/patterns/releases/latest',
80-
isDark: isDark,
128+
onDownload: () => _download(_linuxUrl),
129+
isDark: widget.isDark,
81130
accent: accent,
82131
textColor: textColor,
83132
secondaryText: secondaryText,
84133
border: border,
85-
isBeta: true,
134+
loading: _loading,
86135
),
87136
];
88137

@@ -111,6 +160,13 @@ class DownloadSection extends StatelessWidget {
111160
);
112161
},
113162
),
163+
if (_version != null) ...[
164+
const SizedBox(height: 16),
165+
Text(
166+
'Latest: $_version',
167+
style: GoogleFonts.inter(fontSize: 12, color: secondaryText),
168+
),
169+
],
114170
const SizedBox(height: 32),
115171
// Source code link
116172
GestureDetector(
@@ -152,26 +208,26 @@ class _DownloadCard extends StatefulWidget {
152208
final IconData icon;
153209
final String format;
154210
final String description;
155-
final String url;
211+
final VoidCallback onDownload;
156212
final bool isDark;
157213
final Color accent;
158214
final Color textColor;
159215
final Color secondaryText;
160216
final Color border;
161-
final bool isBeta;
217+
final bool loading;
162218

163219
const _DownloadCard({
164220
required this.platform,
165221
required this.icon,
166222
required this.format,
167223
required this.description,
168-
required this.url,
224+
required this.onDownload,
169225
required this.isDark,
170226
required this.accent,
171227
required this.textColor,
172228
required this.secondaryText,
173229
required this.border,
174-
this.isBeta = false,
230+
this.loading = false,
175231
});
176232

177233
@override
@@ -191,10 +247,8 @@ class _DownloadCardState extends State<_DownloadCard> {
191247
onEnter: (_) => setState(() => _hovered = true),
192248
onExit: (_) => setState(() => _hovered = false),
193249
child: GestureDetector(
194-
onTap: () => launchUrl(Uri.parse(widget.url)),
195-
child: MouseRegion(
196-
cursor: SystemMouseCursors.click,
197-
child: AnimatedContainer(
250+
onTap: widget.onDownload,
251+
child: AnimatedContainer(
198252
duration: const Duration(milliseconds: 250),
199253
constraints: const BoxConstraints(maxWidth: 400),
200254
padding: const EdgeInsets.all(32),
@@ -209,45 +263,15 @@ class _DownloadCardState extends State<_DownloadCard> {
209263
),
210264
child: Column(
211265
children: [
212-
Icon(widget.icon, size: 40,
213-
color: widget.isBeta
214-
? widget.secondaryText
215-
: widget.accent),
266+
Icon(widget.icon, size: 40, color: widget.accent),
216267
const SizedBox(height: 16),
217-
Row(
218-
mainAxisSize: MainAxisSize.min,
219-
children: [
220-
Text(
221-
widget.platform,
222-
style: GoogleFonts.inter(
223-
fontSize: 22,
224-
fontWeight: FontWeight.w700,
225-
color: widget.textColor,
226-
),
227-
),
228-
if (widget.isBeta) ...[
229-
const SizedBox(width: 8),
230-
Container(
231-
padding: const EdgeInsets.symmetric(
232-
horizontal: 8, vertical: 3),
233-
decoration: BoxDecoration(
234-
color: widget.accent.withValues(alpha: 0.15),
235-
borderRadius: BorderRadius.circular(6),
236-
border: Border.all(
237-
color: widget.accent.withValues(alpha: 0.3)),
238-
),
239-
child: Text(
240-
'BETA',
241-
style: GoogleFonts.inter(
242-
fontSize: 10,
243-
fontWeight: FontWeight.w700,
244-
color: widget.accent,
245-
letterSpacing: 1,
246-
),
247-
),
248-
),
249-
],
250-
],
268+
Text(
269+
widget.platform,
270+
style: GoogleFonts.inter(
271+
fontSize: 22,
272+
fontWeight: FontWeight.w700,
273+
color: widget.textColor,
274+
),
251275
),
252276
const SizedBox(height: 4),
253277
Text(
@@ -256,51 +280,42 @@ class _DownloadCardState extends State<_DownloadCard> {
256280
fontSize: 13, color: widget.secondaryText),
257281
),
258282
const SizedBox(height: 20),
259-
if (widget.isBeta)
260-
Container(
261-
padding:
262-
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
263-
decoration: BoxDecoration(
264-
color: widget.secondaryText.withValues(alpha: 0.15),
265-
borderRadius: BorderRadius.circular(10),
266-
),
267-
child: Text(
268-
'Coming Soon',
269-
style: GoogleFonts.inter(
270-
fontSize: 14,
271-
fontWeight: FontWeight.w600,
272-
color: widget.secondaryText,
273-
),
274-
),
275-
)
276-
else
277-
Container(
278-
padding:
279-
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
280-
decoration: BoxDecoration(
281-
color: widget.accent,
282-
borderRadius: BorderRadius.circular(10),
283-
),
284-
child: Row(
285-
mainAxisSize: MainAxisSize.min,
286-
children: [
287-
const Icon(Icons.download_rounded,
288-
size: 16, color: Colors.black),
289-
const SizedBox(width: 8),
290-
Text(
291-
'Download ${widget.format}',
292-
style: GoogleFonts.inter(
293-
fontSize: 14,
294-
fontWeight: FontWeight.w600,
283+
Container(
284+
padding:
285+
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
286+
decoration: BoxDecoration(
287+
color: widget.accent,
288+
borderRadius: BorderRadius.circular(10),
289+
),
290+
child: Row(
291+
mainAxisSize: MainAxisSize.min,
292+
children: [
293+
if (widget.loading)
294+
SizedBox(
295+
width: 14,
296+
height: 14,
297+
child: CircularProgressIndicator(
298+
strokeWidth: 2,
295299
color: Colors.black,
296300
),
301+
)
302+
else
303+
const Icon(Icons.download_rounded,
304+
size: 16, color: Colors.black),
305+
const SizedBox(width: 8),
306+
Text(
307+
'Download ${widget.format}',
308+
style: GoogleFonts.inter(
309+
fontSize: 14,
310+
fontWeight: FontWeight.w600,
311+
color: Colors.black,
297312
),
298-
],
299-
),
313+
),
314+
],
300315
),
316+
),
301317
],
302318
),
303-
),
304319
),
305320
),
306321
);

website/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ packages:
145145
source: hosted
146146
version: "1.0.2"
147147
http:
148-
dependency: transitive
148+
dependency: "direct main"
149149
description:
150150
name: http
151151
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"

website/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dependencies:
3636
cupertino_icons: ^1.0.8
3737
google_fonts: ^8.0.2
3838
url_launcher: ^6.3.2
39+
http: ^1.4.0
3940
font_awesome_flutter: ^11.0.0
4041

4142
dev_dependencies:

0 commit comments

Comments
 (0)