1+ import 'dart:convert' ;
12import 'package:flutter/material.dart' ;
23import 'package:google_fonts/google_fonts.dart' ;
4+ import 'package:http/http.dart' as http;
35import 'package:url_launcher/url_launcher.dart' ;
46import '../theme/web_theme.dart' ;
57import '../widgets/responsive.dart' ;
68import '../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 );
0 commit comments