✨ Basic post item
This commit is contained in:
		
							
								
								
									
										3
									
								
								devtools_options.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								devtools_options.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| description: This file stores settings for Dart & Flutter DevTools. | ||||
| documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states | ||||
| extensions: | ||||
| @@ -13,6 +13,11 @@ PODS: | ||||
|   - shared_preferences_foundation (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - sqflite_darwin (0.0.4): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - url_launcher_ios (0.0.1): | ||||
|     - Flutter | ||||
|  | ||||
| DEPENDENCIES: | ||||
|   - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) | ||||
| @@ -21,6 +26,8 @@ DEPENDENCIES: | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
|   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) | ||||
|   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) | ||||
|   - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) | ||||
|   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) | ||||
|  | ||||
| EXTERNAL SOURCES: | ||||
|   connectivity_plus: | ||||
| @@ -35,6 +42,10 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/path_provider_foundation/darwin" | ||||
|   shared_preferences_foundation: | ||||
|     :path: ".symlinks/plugins/shared_preferences_foundation/darwin" | ||||
|   sqflite_darwin: | ||||
|     :path: ".symlinks/plugins/sqflite_darwin/darwin" | ||||
|   url_launcher_ios: | ||||
|     :path: ".symlinks/plugins/url_launcher_ios/ios" | ||||
|  | ||||
| SPEC CHECKSUMS: | ||||
|   connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 | ||||
| @@ -43,6 +54,8 @@ SPEC CHECKSUMS: | ||||
|   flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 | ||||
|   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 | ||||
|   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 | ||||
|   sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d | ||||
|   url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe | ||||
|  | ||||
| PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:easy_localization_loader/easy_localization_loader.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/theme.dart'; | ||||
| @@ -40,7 +41,10 @@ class SolianApp extends StatelessWidget { | ||||
|               darkTheme: th.theme.dark, | ||||
|               locale: context.locale, | ||||
|               supportedLocales: context.supportedLocales, | ||||
|               localizationsDelegates: context.localizationDelegates, | ||||
|               localizationsDelegates: [ | ||||
|                 RelativeTimeLocalizations.delegate, | ||||
|                 ...context.localizationDelegates, | ||||
|               ], | ||||
|               routerConfig: appRouter, | ||||
|             ); | ||||
|           }), | ||||
|   | ||||
| @@ -27,9 +27,14 @@ class SnNetworkProvider { | ||||
|       ], | ||||
|     )); | ||||
|  | ||||
|     if (!kIsWeb && Platform.isAndroid || Platform.isIOS || Platform.isMacOS) { | ||||
|     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { | ||||
|       // Switch to native implementation if possible | ||||
|       client.httpClientAdapter = NativeAdapter(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   String getAttachmentUrl(String ky) { | ||||
|     if (ky.startsWith("http://")) return ky; | ||||
|     return '${client.options.baseUrl}/cgi/uc/attachments/$ky'; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| class ExploreScreen extends StatefulWidget { | ||||
| @@ -17,16 +18,17 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|   bool _isBusy = true; | ||||
|  | ||||
|   final List<SnPost> _posts = List.empty(growable: true); | ||||
|   int _postCount = 0; | ||||
|   int? _postCount; | ||||
|  | ||||
|   void _fetchPosts() async { | ||||
|     if (_postCount != null && _posts.length >= _postCount!) return; | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     final resp = await sn.client.get('/cgi/co/posts', queryParameters: { | ||||
|       'take': 10, | ||||
|       'offset': 0, | ||||
|       'offset': _posts.length, | ||||
|     }); | ||||
|  | ||||
|     _postCount = resp.data['count']; | ||||
| @@ -51,10 +53,12 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|       body: InfiniteList( | ||||
|         itemCount: _posts.length, | ||||
|         isLoading: _isBusy, | ||||
|         hasReachedMax: _postCount != null && _posts.length >= _postCount!, | ||||
|         onFetchData: _fetchPosts, | ||||
|         itemBuilder: (context, idx) { | ||||
|           return Text(_posts[idx].toString()); | ||||
|           return PostItem(data: _posts[idx]); | ||||
|         }, | ||||
|         separatorBuilder: (context, index) => const Divider(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										49
									
								
								lib/widgets/account/account_image.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/widgets/account/account_image.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class AccountImage extends StatelessWidget { | ||||
|   final String content; | ||||
|   final Color? backgroundColor; | ||||
|   final Color? foregroundColor; | ||||
|   final double? radius; | ||||
|   final Widget? fallbackWidget; | ||||
|  | ||||
|   const AccountImage({ | ||||
|     super.key, | ||||
|     required this.content, | ||||
|     this.backgroundColor, | ||||
|     this.foregroundColor, | ||||
|     this.radius, | ||||
|     this.fallbackWidget, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|     final url = sn.getAttachmentUrl(content); | ||||
|  | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|     return CircleAvatar( | ||||
|       key: Key('attachment-${content.hashCode}'), | ||||
|       radius: radius, | ||||
|       backgroundColor: backgroundColor, | ||||
|       backgroundImage: content.isNotEmpty | ||||
|           ? ResizeImage( | ||||
|               UniversalImage.provider(url), | ||||
|               width: ((radius ?? 20) * devicePixelRatio * 2).round(), | ||||
|               height: ((radius ?? 20) * devicePixelRatio * 2).round(), | ||||
|             ) | ||||
|           : null, | ||||
|       child: content.isEmpty | ||||
|           ? (fallbackWidget ?? | ||||
|               Icon( | ||||
|                 Icons.account_circle, | ||||
|                 size: radius != null ? radius! * 1.2 : 24, | ||||
|                 color: foregroundColor, | ||||
|               )) | ||||
|           : null, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										183
									
								
								lib/widgets/markdown_content.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								lib/widgets/markdown_content.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:markdown/markdown.dart' as markdown; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:syntax_highlight/syntax_highlight.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:path/path.dart'; | ||||
|  | ||||
| class MarkdownTextContent extends StatefulWidget { | ||||
|   final String content; | ||||
|   final bool isSelectable; | ||||
|   final bool isLargeText; | ||||
|   final bool isAutoWarp; | ||||
|  | ||||
|   const MarkdownTextContent({ | ||||
|     super.key, | ||||
|     required this.content, | ||||
|     this.isSelectable = false, | ||||
|     this.isLargeText = false, | ||||
|     this.isAutoWarp = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<MarkdownTextContent> createState() => _MarkdownTextContentState(); | ||||
| } | ||||
|  | ||||
| class _MarkdownTextContentState extends State<MarkdownTextContent> { | ||||
|   Widget _buildContent(BuildContext context) { | ||||
|     return Markdown( | ||||
|       shrinkWrap: true, | ||||
|       physics: const NeverScrollableScrollPhysics(), | ||||
|       data: widget.content, | ||||
|       padding: EdgeInsets.zero, | ||||
|       styleSheet: MarkdownStyleSheet.fromTheme( | ||||
|         Theme.of(context), | ||||
|       ).copyWith( | ||||
|           textScaler: TextScaler.linear(widget.isLargeText ? 1.1 : 1), | ||||
|           blockquote: TextStyle( | ||||
|             color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|           ), | ||||
|           blockquoteDecoration: BoxDecoration( | ||||
|             color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|           ), | ||||
|           horizontalRuleDecoration: BoxDecoration( | ||||
|             border: Border( | ||||
|               top: BorderSide( | ||||
|                 width: 1.0, | ||||
|                 color: Theme.of(context).dividerColor, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           codeblockDecoration: BoxDecoration( | ||||
|             border: Border.all( | ||||
|               color: Theme.of(context).dividerColor, | ||||
|               width: 0.3, | ||||
|             ), | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|             color: Theme.of(context).colorScheme.surface.withOpacity(0.5), | ||||
|           )), | ||||
|       builders: { | ||||
|         'code': _MarkdownTextCodeElement(), | ||||
|       }, | ||||
|       softLineBreak: true, | ||||
|       extensionSet: markdown.ExtensionSet( | ||||
|         <markdown.BlockSyntax>[ | ||||
|           markdown.CodeBlockSyntax(), | ||||
|           ...markdown.ExtensionSet.commonMark.blockSyntaxes, | ||||
|           ...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, | ||||
|         ], | ||||
|         <markdown.InlineSyntax>[ | ||||
|           if (widget.isAutoWarp) markdown.LineBreakSyntax(), | ||||
|           _UserNameCardInlineSyntax(), | ||||
|           markdown.AutolinkSyntax(), | ||||
|           markdown.AutolinkExtensionSyntax(), | ||||
|           markdown.CodeSyntax(), | ||||
|           ...markdown.ExtensionSet.commonMark.inlineSyntaxes, | ||||
|           ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes | ||||
|         ], | ||||
|       ), | ||||
|       onTapLink: (text, href, title) async { | ||||
|         if (href == null) return; | ||||
|         if (href.startsWith('solink://')) { | ||||
|           // final segments = href.replaceFirst('solink://', '').split('/'); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         await launchUrlString( | ||||
|           href, | ||||
|           mode: LaunchMode.externalApplication, | ||||
|         ); | ||||
|       }, | ||||
|       imageBuilder: (uri, title, alt) { | ||||
|         var url = uri.toString(); | ||||
|         double? width, height; | ||||
|         BoxFit? fit; | ||||
|         if (url.startsWith('solink://')) { | ||||
|           // final segments = url.replaceFirst('solink://', '').split('/'); | ||||
|           return const SizedBox.shrink(); | ||||
|         } | ||||
|         return UniversalImage( | ||||
|           url, | ||||
|           width: width, | ||||
|           height: height, | ||||
|           fit: fit, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (widget.isSelectable) { | ||||
|       return SelectionArea(child: _buildContent(context)); | ||||
|     } | ||||
|     return _buildContent(context); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _UserNameCardInlineSyntax extends markdown.InlineSyntax { | ||||
|   _UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+'); | ||||
|  | ||||
|   @override | ||||
|   bool onMatch(markdown.InlineParser parser, Match match) { | ||||
|     final alias = match[0]!; | ||||
|     final anchor = markdown.Element.text('a', alias) | ||||
|       ..attributes['href'] = Uri.encodeFull( | ||||
|         'solink://users/${alias.substring(1)}', | ||||
|       ); | ||||
|     parser.addNode(anchor); | ||||
|  | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _MarkdownTextCodeElement extends MarkdownElementBuilder { | ||||
|   @override | ||||
|   Widget? visitElementAfter( | ||||
|     markdown.Element element, | ||||
|     TextStyle? preferredStyle, | ||||
|   ) { | ||||
|     var language = ''; | ||||
|  | ||||
|     if (element.attributes['class'] != null) { | ||||
|       String lg = element.attributes['class'] as String; | ||||
|       language = lg.substring(9).trim(); | ||||
|     } | ||||
|     return SizedBox( | ||||
|       child: FutureBuilder( | ||||
|         future: (() async { | ||||
|           final docPath = '../../../'; | ||||
|           final highlightingPath = | ||||
|               join(docPath, 'assets/highlighting', language); | ||||
|           await Highlighter.initialize([highlightingPath]); | ||||
|           return Highlighter( | ||||
|             language: highlightingPath, | ||||
|             theme: PlatformDispatcher.instance.platformBrightness == | ||||
|                     Brightness.light | ||||
|                 ? await HighlighterTheme.loadLightTheme() | ||||
|                 : await HighlighterTheme.loadDarkTheme(), | ||||
|           ); | ||||
|         })(), | ||||
|         builder: (context, snapshot) { | ||||
|           if (snapshot.hasData) { | ||||
|             final highlighter = snapshot.data!; | ||||
|             return Text.rich( | ||||
|               highlighter.highlight(element.textContent.trim()), | ||||
|               style: GoogleFonts.robotoMono(), | ||||
|             ); | ||||
|           } | ||||
|           return Text( | ||||
|             element.textContent.trim(), | ||||
|             style: GoogleFonts.robotoMono(), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ).padding(all: 8); | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,11 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/markdown_content.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
|  | ||||
| class PostItem extends StatelessWidget { | ||||
|   final SnPost data; | ||||
| @@ -8,16 +14,61 @@ class PostItem extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Column( | ||||
|       children: [], | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         _PostContentHeader(data: data), | ||||
|         _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostContentPublisher extends StatelessWidget { | ||||
|   const _PostContentPublisher({super.key}); | ||||
| class _PostContentHeader extends StatelessWidget { | ||||
|   final SnPost data; | ||||
|   const _PostContentHeader({required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return const Placeholder(); | ||||
|     return Row( | ||||
|       children: [ | ||||
|         AccountImage(content: data.publisher.avatar), | ||||
|         const Gap(12), | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Text(data.publisher.nick).bold(), | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Text('@${data.publisher.name}').fontSize(13), | ||||
|                   const Gap(4), | ||||
|                   Text(RelativeTime(context).format(data.publishedAt)) | ||||
|                       .fontSize(13), | ||||
|                 ], | ||||
|               ).opacity(0.8), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         IconButton( | ||||
|           onPressed: () {}, | ||||
|           visualDensity: const VisualDensity(horizontal: -4, vertical: -4), | ||||
|           icon: const Icon( | ||||
|             Symbols.more_horiz, | ||||
|             size: 16, | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ).padding(horizontal: 12, vertical: 8); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostContentBody extends StatelessWidget { | ||||
|   final dynamic data; | ||||
|   const _PostContentBody({this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (data['content'] == null) return const SizedBox.shrink(); | ||||
|     return MarkdownTextContent(content: data['content']); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										147
									
								
								lib/widgets/universal_image.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								lib/widgets/universal_image.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:cached_network_image/cached_network_image.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
|  | ||||
| class UniversalImage extends StatelessWidget { | ||||
|   final String url; | ||||
|   final double? width, height; | ||||
|   final BoxFit? fit; | ||||
|   final bool noProgressIndicator; | ||||
|   final bool noErrorWidget; | ||||
|   final double? cacheWidth, cacheHeight; | ||||
|  | ||||
|   const UniversalImage( | ||||
|     this.url, { | ||||
|     super.key, | ||||
|     this.width, | ||||
|     this.height, | ||||
|     this.fit, | ||||
|     this.noProgressIndicator = false, | ||||
|     this.noErrorWidget = false, | ||||
|     this.cacheWidth, | ||||
|     this.cacheHeight, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { | ||||
|       return CachedNetworkImage( | ||||
|         imageUrl: url, | ||||
|         width: width, | ||||
|         height: height, | ||||
|         fit: fit, | ||||
|         memCacheHeight: cacheHeight != null | ||||
|             ? (cacheHeight! * devicePixelRatio).round() | ||||
|             : null, | ||||
|         memCacheWidth: cacheWidth != null | ||||
|             ? (cacheWidth! * devicePixelRatio).round() | ||||
|             : null, | ||||
|         progressIndicatorBuilder: noProgressIndicator | ||||
|             ? null | ||||
|             : (context, url, downloadProgress) => Center( | ||||
|                   child: TweenAnimationBuilder( | ||||
|                     tween: Tween( | ||||
|                       begin: 0, | ||||
|                       end: downloadProgress.progress ?? 0, | ||||
|                     ), | ||||
|                     duration: const Duration(milliseconds: 300), | ||||
|                     builder: (context, value, _) => CircularProgressIndicator( | ||||
|                       value: downloadProgress.progress != null | ||||
|                           ? value.toDouble() | ||||
|                           : null, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|         errorWidget: noErrorWidget | ||||
|             ? null | ||||
|             : (context, url, error) { | ||||
|                 return Container( | ||||
|                   color: Theme.of(context).colorScheme.surface, | ||||
|                   constraints: const BoxConstraints(maxWidth: 280), | ||||
|                   child: Column( | ||||
|                     mainAxisAlignment: MainAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       AnimateWidgetExtensions(Icon(Icons.close, size: 24)) | ||||
|                           .animate(onPlay: (e) => e.repeat(reverse: true)) | ||||
|                           .fade(duration: 500.ms), | ||||
|                       Text( | ||||
|                         error.toString(), | ||||
|                         textAlign: TextAlign.center, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).center(), | ||||
|                 ); | ||||
|               }, | ||||
|       ); | ||||
|     } | ||||
|     return Image.network( | ||||
|       url, | ||||
|       width: width, | ||||
|       height: height, | ||||
|       fit: fit, | ||||
|       cacheHeight: cacheHeight != null | ||||
|           ? (cacheHeight! * devicePixelRatio).round() | ||||
|           : null, | ||||
|       cacheWidth: | ||||
|           cacheWidth != null ? (cacheWidth! * devicePixelRatio).round() : null, | ||||
|       loadingBuilder: noProgressIndicator | ||||
|           ? null | ||||
|           : (BuildContext context, Widget child, | ||||
|               ImageChunkEvent? loadingProgress) { | ||||
|               if (loadingProgress == null) return child; | ||||
|               return Center( | ||||
|                 child: TweenAnimationBuilder( | ||||
|                   tween: Tween( | ||||
|                     begin: 0, | ||||
|                     end: loadingProgress.expectedTotalBytes != null | ||||
|                         ? loadingProgress.cumulativeBytesLoaded / | ||||
|                             loadingProgress.expectedTotalBytes! | ||||
|                         : 0, | ||||
|                   ), | ||||
|                   duration: const Duration(milliseconds: 300), | ||||
|                   builder: (context, value, _) => CircularProgressIndicator( | ||||
|                     value: loadingProgress.expectedTotalBytes != null | ||||
|                         ? value.toDouble() | ||||
|                         : null, | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|       errorBuilder: noErrorWidget | ||||
|           ? null | ||||
|           : (context, error, stackTrace) { | ||||
|               return Material( | ||||
|                 color: Theme.of(context).colorScheme.surface, | ||||
|                 child: Container( | ||||
|                   constraints: const BoxConstraints(maxWidth: 280), | ||||
|                   child: Column( | ||||
|                     mainAxisAlignment: MainAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       AnimateWidgetExtensions(Icon(Icons.close, size: 24)) | ||||
|                           .animate(onPlay: (e) => e.repeat(reverse: true)) | ||||
|                           .fade(duration: 500.ms), | ||||
|                       Text( | ||||
|                         error.toString(), | ||||
|                         textAlign: TextAlign.center, | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).center(), | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static ImageProvider provider(String url) { | ||||
|     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { | ||||
|       return CachedNetworkImageProvider(url); | ||||
|     } | ||||
|     return NetworkImage(url); | ||||
|   } | ||||
| } | ||||
| @@ -6,6 +6,10 @@ | ||||
|  | ||||
| #include "generated_plugin_registrant.h" | ||||
|  | ||||
| #include <url_launcher_linux/url_launcher_plugin.h> | ||||
|  | ||||
| void fl_register_plugins(FlPluginRegistry* registry) { | ||||
|   g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); | ||||
|   url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| # | ||||
|  | ||||
| list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   url_launcher_linux | ||||
| ) | ||||
|  | ||||
| list(APPEND FLUTTER_FFI_PLUGIN_LIST | ||||
|   | ||||
| @@ -8,9 +8,13 @@ import Foundation | ||||
| import connectivity_plus | ||||
| import path_provider_foundation | ||||
| import shared_preferences_foundation | ||||
| import sqflite_darwin | ||||
| import url_launcher_macos | ||||
|  | ||||
| func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) | ||||
|   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) | ||||
|   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) | ||||
|   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||
|   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) | ||||
| } | ||||
|   | ||||
							
								
								
									
										238
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										238
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -134,6 +134,30 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.9.2" | ||||
|   cached_network_image: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: cached_network_image | ||||
|       sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.4.1" | ||||
|   cached_network_image_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: cached_network_image_platform_interface | ||||
|       sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.1.1" | ||||
|   cached_network_image_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: cached_network_image_web | ||||
|       sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.1" | ||||
|   characters: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -347,6 +371,22 @@ packages: | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.0" | ||||
|   flutter_animate: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_animate | ||||
|       sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.5.0" | ||||
|   flutter_cache_manager: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_cache_manager | ||||
|       sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.4.1" | ||||
|   flutter_lints: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -360,6 +400,14 @@ packages: | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.0" | ||||
|   flutter_markdown: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_markdown | ||||
|       sha256: f0e599ba89c9946c8e051780f0ec99aba4ba15895e0380a7ab68f420046fc44e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.4+1" | ||||
|   flutter_native_splash: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -368,6 +416,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.2" | ||||
|   flutter_shaders: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_shaders | ||||
|       sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.3" | ||||
|   flutter_test: | ||||
|     dependency: "direct dev" | ||||
|     description: flutter | ||||
| @@ -426,6 +482,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "14.4.1" | ||||
|   google_fonts: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: google_fonts | ||||
|       sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.2.1" | ||||
|   graphs: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -594,6 +658,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.3-main.0" | ||||
|   markdown: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: markdown | ||||
|       sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.2.2" | ||||
|   matcher: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -614,10 +686,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: material_symbols_icons | ||||
|       sha256: "7626ce90395bc6dc2ecb7bdd84c04a97f3f084a4e923ff73791c3c409af02804" | ||||
|       sha256: "7b723abea4ad37e16fe921f1f1971cbb9b0f66d223a8c99981168a2306416b98" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2789.0" | ||||
|     version: "4.2791.1" | ||||
|   meta: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -658,6 +730,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.0" | ||||
|   octo_image: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: octo_image | ||||
|       sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|   package_config: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -667,7 +747,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|   path: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: path | ||||
|       sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" | ||||
| @@ -778,6 +858,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.0" | ||||
|   relative_time: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: relative_time | ||||
|       sha256: "4e6c3b27d98ff6af5061b6dbaca178b5aa2607b7a7c2a77e6cae1d32b2759893" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.0.0" | ||||
|   responsive_framework: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -786,6 +874,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.5.1" | ||||
|   rxdart: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: rxdart | ||||
|       sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.28.0" | ||||
|   shared_preferences: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -887,6 +983,54 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.10.0" | ||||
|   sprintf: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sprintf | ||||
|       sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.0.0" | ||||
|   sqflite: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite | ||||
|       sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.1" | ||||
|   sqflite_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_android | ||||
|       sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.0" | ||||
|   sqflite_common: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_common | ||||
|       sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.5.4+5" | ||||
|   sqflite_darwin: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_darwin | ||||
|       sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.1" | ||||
|   sqflite_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_platform_interface | ||||
|       sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.0" | ||||
|   stack_trace: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -927,6 +1071,22 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.4.1" | ||||
|   synchronized: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: synchronized | ||||
|       sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.3.0+3" | ||||
|   syntax_highlight: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: syntax_highlight | ||||
|       sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.4.0" | ||||
|   term_glyph: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -967,6 +1127,78 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.2" | ||||
|   url_launcher: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: url_launcher | ||||
|       sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.3.1" | ||||
|   url_launcher_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.3.14" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_ios | ||||
|       sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.3.1" | ||||
|   url_launcher_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_linux | ||||
|       sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.2.0" | ||||
|   url_launcher_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_macos | ||||
|       sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.2.1" | ||||
|   url_launcher_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_platform_interface | ||||
|       sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.2" | ||||
|   url_launcher_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_web | ||||
|       sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.3" | ||||
|   url_launcher_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_windows | ||||
|       sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.1.3" | ||||
|   uuid: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: uuid | ||||
|       sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.5.1" | ||||
|   vector_math: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -49,6 +49,15 @@ dependencies: | ||||
|   freezed_annotation: ^2.4.4 | ||||
|   json_annotation: ^4.9.0 | ||||
|   gap: ^3.0.1 | ||||
|   markdown: ^7.2.2 | ||||
|   flutter_markdown: ^0.7.4+1 | ||||
|   url_launcher: ^6.3.1 | ||||
|   cached_network_image: ^3.4.1 | ||||
|   flutter_animate: ^4.5.0 | ||||
|   syntax_highlight: ^0.4.0 | ||||
|   google_fonts: ^6.2.1 | ||||
|   path: ^1.9.0 | ||||
|   relative_time: ^5.0.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -7,8 +7,11 @@ | ||||
| #include "generated_plugin_registrant.h" | ||||
|  | ||||
| #include <connectivity_plus/connectivity_plus_windows_plugin.h> | ||||
| #include <url_launcher_windows/url_launcher_windows.h> | ||||
|  | ||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|   ConnectivityPlusWindowsPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); | ||||
|   UrlLauncherWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|  | ||||
| list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   connectivity_plus | ||||
|   url_launcher_windows | ||||
| ) | ||||
|  | ||||
| list(APPEND FLUTTER_FFI_PLUGIN_LIST | ||||
|   | ||||
		Reference in New Issue
	
	Block a user