From 40d251f400613456391f73c19c966d3dcf5dad16 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Wed, 19 Mar 2025 00:44:14 +0100 Subject: [PATCH] Jesus they're not BBCode... They're html and bbcode and markdown AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA --- lib/bbcode_converter.dart | 8 +- lib/components/html_tooltip.dart | 327 ++++++++++++++++++ lib/format_converter.dart | 257 ++++++++++++++ lib/main.dart | 16 +- lib/widgets/mod_card_example.dart | 95 +++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 111 +++++- pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 818 insertions(+), 9 deletions(-) create mode 100644 lib/components/html_tooltip.dart create mode 100644 lib/format_converter.dart create mode 100644 lib/widgets/mod_card_example.dart diff --git a/lib/bbcode_converter.dart b/lib/bbcode_converter.dart index 7969067..b166ddb 100644 --- a/lib/bbcode_converter.dart +++ b/lib/bbcode_converter.dart @@ -1,4 +1,6 @@ /// Utility class to convert BBCode to Markdown for RimWorld mod descriptions +library bbcode_converter; + import 'dart:math' as math; class BBCodeConverter { @@ -21,10 +23,10 @@ class BBCodeConverter { // Fix unclosed tags - RimWorld descriptions often have unclosed tags final List tagTypes = ['b', 'i', 'color', 'size', 'url', 'code', 'quote']; for (final tag in tagTypes) { - final openCount = '[${tag}'.allMatches(result).length; - final closeCount = '[/${tag}]'.allMatches(result).length; + final openCount = '[$tag'.allMatches(result).length; + final closeCount = '[/$tag]'.allMatches(result).length; if (openCount > closeCount) { - result = result + '[/${tag}]' * (openCount - closeCount); + result = result + '[/$tag]' * (openCount - closeCount); } } diff --git a/lib/components/html_tooltip.dart b/lib/components/html_tooltip.dart new file mode 100644 index 0000000..802b53f --- /dev/null +++ b/lib/components/html_tooltip.dart @@ -0,0 +1,327 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../format_converter.dart'; +import 'dart:math' as math; + +class HtmlTooltip extends StatefulWidget { + final Widget child; + final String content; + final double maxWidth; + final double maxHeight; + final EdgeInsets padding; + final Duration showDuration; + final Duration fadeDuration; + final Color backgroundColor; + final Color textColor; + final BorderRadius borderRadius; + final bool preferBelow; + final String? title; + + const HtmlTooltip({ + super.key, + required this.child, + required this.content, + this.maxWidth = 300.0, + this.maxHeight = 400.0, + this.padding = const EdgeInsets.all(8.0), + this.showDuration = const Duration(milliseconds: 0), + this.fadeDuration = const Duration(milliseconds: 200), + this.backgroundColor = const Color(0xFF232323), + this.textColor = Colors.white, + this.borderRadius = const BorderRadius.all(Radius.circular(4.0)), + this.preferBelow = true, + this.title = 'Mod Description', + }); + + @override + State createState() => _HtmlTooltipState(); +} + +class _HtmlTooltipState extends State { + final LayerLink _layerLink = LayerLink(); + OverlayEntry? _overlayEntry; + bool _isTooltipVisible = false; + bool _isMouseInside = false; + bool _isMouseInsideTooltip = false; + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + _hideTooltip(); + super.dispose(); + } + + // Launch a URL + Future _launchUrl(String? urlString) async { + if (urlString == null || urlString.isEmpty) return; + + final Uri url = Uri.parse(urlString); + try { + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } + } catch (e) { + debugPrint('Error launching URL: $e'); + } + } + + void _showTooltip(BuildContext context) { + if (_overlayEntry != null) return; + + // Get render box of the trigger widget + final RenderBox box = context.findRenderObject() as RenderBox; + final Size childSize = box.size; + + // Calculate appropriate width and position + final screenWidth = MediaQuery.of(context).size.width; + double tooltipWidth = widget.maxWidth; + + // Adjust tooltip width for longer descriptions + if (widget.content.length > 1000) { + tooltipWidth = math.min(screenWidth * 0.5, 500.0); + } + + _overlayEntry = OverlayEntry( + builder: (context) { + return Positioned( + width: tooltipWidth, + child: CompositedTransformFollower( + link: _layerLink, + showWhenUnlinked: false, + offset: Offset( + (childSize.width / 2) - (tooltipWidth / 2), + widget.preferBelow ? childSize.height + 5 : -5, + ), + child: Material( + color: Colors.transparent, + child: MouseRegion( + onEnter: (_) { + setState(() { + _isMouseInsideTooltip = true; + }); + }, + onExit: (_) { + setState(() { + _isMouseInsideTooltip = false; + // Slight delay to prevent flickering + Future.delayed(const Duration(milliseconds: 50), () { + if (!_isMouseInside && !_isMouseInsideTooltip) { + _hideTooltip(); + } + }); + }); + }, + child: FadeTransition( + opacity: const AlwaysStoppedAnimation(1.0), + child: Container( + constraints: BoxConstraints( + maxWidth: tooltipWidth, + maxHeight: widget.maxHeight, + ), + decoration: BoxDecoration( + color: widget.backgroundColor, + borderRadius: widget.borderRadius, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(77), // Equivalent to 0.3 opacity + blurRadius: 10.0, + spreadRadius: 0.0, + ), + ], + ), + child: ClipRRect( + borderRadius: widget.borderRadius, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + color: const Color(0xFF3D4A59), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title ?? 'Description', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14.0, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Scroll to top button + InkWell( + onTap: () { + if (_scrollController.hasClients) { + _scrollController.animateTo( + 0.0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }, + child: const Padding( + padding: EdgeInsets.all(2.0), + child: Icon( + Icons.arrow_upward, + color: Colors.white, + size: 16.0, + ), + ), + ), + const SizedBox(width: 4.0), + // Close button + InkWell( + onTap: () { + _hideTooltip(); + }, + child: const Padding( + padding: EdgeInsets.all(2.0), + child: Icon( + Icons.close, + color: Colors.white, + size: 16.0, + ), + ), + ), + ], + ), + ], + ), + ), + + // Content + Flexible( + child: SingleChildScrollView( + controller: _scrollController, + child: Padding( + padding: widget.padding, + child: Html( + data: FormatConverter.toHtml(widget.content), + style: { + "body": Style( + color: widget.textColor, + margin: Margins.zero, + padding: HtmlPaddings.zero, + fontSize: FontSize(14.0), + ), + "a": Style( + color: Colors.lightBlue, + textDecoration: TextDecoration.underline, + ), + "blockquote": Style( + backgroundColor: Colors.grey.withAlpha(26), // Approx 0.1 opacity + border: Border( + left: BorderSide( + color: Colors.grey.withAlpha(128), // Approx 0.5 opacity + width: 4.0, + ), + ), + padding: HtmlPaddings.all(8.0), + margin: Margins.only(left: 0, right: 0), + ), + "code": Style( + backgroundColor: Colors.grey.withAlpha(51), // Approx 0.2 opacity + padding: HtmlPaddings.all(2.0), + fontFamily: 'monospace', + ), + "pre": Style( + backgroundColor: Colors.grey.withAlpha(51), // Approx 0.2 opacity + padding: HtmlPaddings.all(8.0), + fontFamily: 'monospace', + margin: Margins.only(bottom: 8.0, top: 8.0), + ), + "table": Style( + border: Border.all(color: Colors.grey), + backgroundColor: Colors.transparent, + ), + "td": Style( + border: Border.all(color: Colors.grey.withAlpha(128)), // Approx 0.5 opacity + padding: HtmlPaddings.all(4.0), + ), + "th": Style( + border: Border.all(color: Colors.grey.withAlpha(128)), // Approx 0.5 opacity + padding: HtmlPaddings.all(4.0), + backgroundColor: Colors.grey.withAlpha(51), // Approx 0.2 opacity + ), + }, + onAnchorTap: (url, _, __) { + _launchUrl(url); + }, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); + + // Use a check for mounted before inserting the overlay + if (mounted) { + Overlay.of(context).insert(_overlayEntry!); + _isTooltipVisible = true; + } + } + + void _hideTooltip() { + _overlayEntry?.remove(); + _overlayEntry = null; + _isTooltipVisible = false; + } + + @override + Widget build(BuildContext context) { + return CompositedTransformTarget( + link: _layerLink, + child: MouseRegion( + onEnter: (_) { + setState(() { + _isMouseInside = true; + // Show tooltip after a brief delay to prevent accidental triggers + Future.delayed(const Duration(milliseconds: 50), () { + if (mounted && _isMouseInside && !_isTooltipVisible) { + _showTooltip(context); + } + }); + }); + }, + onExit: (_) { + setState(() { + _isMouseInside = false; + // Slight delay to prevent flickering + Future.delayed(const Duration(milliseconds: 50), () { + if (mounted && !_isMouseInside && !_isMouseInsideTooltip) { + _hideTooltip(); + } + }); + }); + }, + child: GestureDetector( + onTap: () { + // Toggle tooltip for touch devices + if (_isTooltipVisible) { + _hideTooltip(); + } else { + _showTooltip(context); + } + }, + child: widget.child, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/format_converter.dart b/lib/format_converter.dart new file mode 100644 index 0000000..dae321c --- /dev/null +++ b/lib/format_converter.dart @@ -0,0 +1,257 @@ +import 'dart:math' as math; + +/// Utility class to convert mixed format content (BBCode, Markdown, and HTML) to HTML +class FormatConverter { + /// Converts mixed format text (BBCode, Markdown, HTML) to pure HTML + static String toHtml(String content) { + if (content.isEmpty) return ''; + + // First, normalize line endings and escape any literal backslashes that aren't already escaped + String result = content.replaceAll('\r\n', '\n'); + + // Handle BBCode format + result = _convertBBCodeToHtml(result); + + // Handle Markdown format + result = _convertMarkdownToHtml(result); + + // Sanitize HTML + result = _sanitizeHtml(result); + + // Wrap the final content in a container with styles + result = '
$result
'; + + return result; + } + + /// Converts BBCode to HTML + static String _convertBBCodeToHtml(String bbcode) { + String result = bbcode; + + // Fix unclosed tags - RimWorld descriptions often have unclosed BBCode tags + final List tagTypes = ['b', 'i', 'color', 'size', 'url', 'code', 'quote', 'list', 'table', 'tr', 'td']; + for (final tag in tagTypes) { + final openCount = '[${tag}'.allMatches(result).length; + final closeCount = '[/$tag]'.allMatches(result).length; + if (openCount > closeCount) { + result = result + '[/$tag]' * (openCount - closeCount); + } + } + + // URLs + // [url=http://example.com]text[/url] -> text + result = RegExp(r'\[url=([^\]]+)\](.*?)\[/url\]', dotAll: true) + .allMatches(result) + .fold(result, (prev, match) { + final url = match.group(1); + final text = match.group(2); + return prev.replaceFirst( + match.group(0)!, + '$text' + ); + }); + + // Simple URL [url]http://example.com[/url] -> http://example.com + result = result.replaceAllMapped( + RegExp(r'\[url\](.*?)\[/url\]', dotAll: true), + (match) => '${match.group(1)}' + ); + + // Bold + result = result.replaceAll('[b]', '').replaceAll('[/b]', ''); + + // Italic + result = result.replaceAll('[i]', '').replaceAll('[/i]', ''); + + // Headers + result = result.replaceAllMapped( + RegExp(r'\[h1\](.*?)\[/h1\]', dotAll: true), + (match) => '

${match.group(1)?.trim()}

' + ); + result = result.replaceAllMapped( + RegExp(r'\[h2\](.*?)\[/h2\]', dotAll: true), + (match) => '

${match.group(1)?.trim()}

' + ); + result = result.replaceAllMapped( + RegExp(r'\[h3\](.*?)\[/h3\]', dotAll: true), + (match) => '

${match.group(1)?.trim()}

' + ); + + // Lists + result = result.replaceAll('[list]', '
    ').replaceAll('[/list]', '
'); + + // List items + result = result.replaceAllMapped( + RegExp(r'\[\*\](.*?)(?=\[\*\]|\[/list\]|$)', dotAll: true), + (match) { + final content = match.group(1)?.trim() ?? ''; + return '
  • $content
  • '; + } + ); + + // Color + result = result.replaceAllMapped( + RegExp(r'\[color=([^\]]+)\](.*?)\[/color\]', dotAll: true), + (match) { + final color = match.group(1) ?? ''; + final content = match.group(2) ?? ''; + if (content.trim().isEmpty) return ''; + return '$content'; + } + ); + + // Images + result = result.replaceAllMapped( + RegExp(r'\[img\](.*?)\[/img\]', dotAll: true), + (match) => 'Image' + ); + + // Image with size + result = result.replaceAllMapped( + RegExp(r'\[img[^\]]*width=(\d+)[^\]]*\](.*?)\[/img\]', dotAll: true), + (match) { + final width = match.group(1) ?? ''; + final url = match.group(2) ?? ''; + return 'Image'; + } + ); + + // Tables + result = result.replaceAll('[table]', '').replaceAll('[/table]', '
    '); + result = result.replaceAll('[tr]', '').replaceAll('[/tr]', ''); + result = result.replaceAll('[td]', '').replaceAll('[/td]', ''); + + // Size + result = result.replaceAllMapped( + RegExp(r'\[size=([^\]]+)\](.*?)\[/size\]', dotAll: true), + (match) { + final size = match.group(1) ?? ''; + final content = match.group(2) ?? ''; + return '$content'; + } + ); + + // Code + result = result.replaceAll('[code]', '
    ').replaceAll('[/code]', '
    '); + + // Quote + result = result.replaceAllMapped( + RegExp(r'\[quote\](.*?)\[/quote\]', dotAll: true), + (match) { + final content = match.group(1)?.trim() ?? ''; + if (content.isEmpty) return ''; + return '
    $content
    '; + } + ); + + // Handle any remaining custom BBCode tags + result = result.replaceAllMapped( + RegExp(r'\[([a-zA-Z0-9_]+)(?:=[^\]]+)?\](.*?)\[/\1\]', dotAll: true), + (match) => match.group(2) ?? '' + ); + + // Handle RimWorld-specific patterns + // [h1] without closing tag is common + result = result.replaceAllMapped( + RegExp(r'\[h1\]([^\[]+)'), + (match) => '

    ${match.group(1)?.trim()}

    ' + ); + + return result; + } + + /// Converts Markdown to HTML + static String _convertMarkdownToHtml(String markdown) { + String result = markdown; + + // Headers + // Convert # Header to

    Header

    + result = result.replaceAllMapped( + RegExp(r'^#\s+(.*?)$', multiLine: true), + (match) => '

    ${match.group(1)?.trim()}

    ' + ); + + // Convert ## Header to

    Header

    + result = result.replaceAllMapped( + RegExp(r'^##\s+(.*?)$', multiLine: true), + (match) => '

    ${match.group(1)?.trim()}

    ' + ); + + // Convert ### Header to

    Header

    + result = result.replaceAllMapped( + RegExp(r'^###\s+(.*?)$', multiLine: true), + (match) => '

    ${match.group(1)?.trim()}

    ' + ); + + // Bold - **text** to text + result = result.replaceAllMapped( + RegExp(r'\*\*(.*?)\*\*'), + (match) => '${match.group(1)}' + ); + + // Italic - *text* or _text_ to text + result = result.replaceAllMapped( + RegExp(r'\*(.*?)\*|_(.*?)_'), + (match) => '${match.group(1) ?? match.group(2)}' + ); + + // Inline code - `code` to code + result = result.replaceAllMapped( + RegExp(r'`(.*?)`'), + (match) => '${match.group(1)}' + ); + + // Links - [text](url) to text + result = result.replaceAllMapped( + RegExp(r'\[(.*?)\]\((.*?)\)'), + (match) => '${match.group(1)}' + ); + + // Images - ![alt](url) to alt + result = result.replaceAllMapped( + RegExp(r'!\[(.*?)\]\((.*?)\)'), + (match) => '${match.group(1)}' + ); + + // Lists - Convert Markdown bullet lists to HTML lists + // This is a simple implementation and might not handle all cases + result = result.replaceAllMapped( + RegExp(r'^(\s*)\*\s+(.*?)$', multiLine: true), + (match) => '
  • ${match.group(2)}
  • ' + ); + + // Wrap adjacent list items in
      tags (simple approach) + result = result.replaceAll('\n
    • ', '
    • '); + result = result.replaceAll('
    • ', '
      • '); + result = result.replaceAll('
      • ', '
      '); + + // Remove duplicated
      tags + result = result.replaceAll('
      ', ''); + + // Paragraphs - Convert newlines to
      , but skipping where tags already exist + result = result.replaceAllMapped( + RegExp(r'(?)\n(?!<)'), + (match) => '
      ' + ); + + return result; + } + + /// Performs basic sanitization and fixes for the HTML + static String _sanitizeHtml(String html) { + // Remove potentially dangerous elements and attributes + final String result = html + // Remove any script tags + .replaceAll(RegExp(r'.*?', dotAll: true), '') + // Remove on* event handlers + .replaceAll(RegExp(r'\son\w+=".*?"'), '') + // Ensure newlines are converted to
      if not already handled + .replaceAll(RegExp(r'(?)\n(?!<)'), '
      '); + + // Fix double paragraph or break issues + return result + .replaceAll('

      ', '
      ') + .replaceAll('

      ', '
      ') + .replaceAll('

      ', ''); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d87d18c..142f0dd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:rimworld_modman/logger.dart'; -import 'package:rimworld_modman/markdown_tooltip.dart'; +import 'package:rimworld_modman/components/html_tooltip.dart'; import 'package:rimworld_modman/mod.dart'; import 'package:rimworld_modman/mod_list.dart'; import 'package:rimworld_modman/mod_troubleshooter_widget.dart'; +import 'package:rimworld_modman/widgets/mod_card_example.dart'; // Theme extension to store app-specific constants class AppThemeExtension extends ThemeExtension { @@ -244,6 +245,7 @@ class _ModManagerHomePageState extends State { final List _pages = [ const ModManagerPage(), const TroubleshootingPage(), + const ModCardExample(), ]; @override @@ -264,6 +266,10 @@ class _ModManagerHomePageState extends State { icon: Icon(Icons.build), label: 'Troubleshoot', ), + BottomNavigationBarItem( + icon: Icon(Icons.format_paint), + label: 'Demo', + ), ], ), ); @@ -744,8 +750,8 @@ class _ModManagerPageState extends State { children: [ // Description tooltip if (mod.description.isNotEmpty) - MarkdownTooltip( - markdownContent: mod.description, + HtmlTooltip( + content: mod.description, child: Icon( Icons.description_outlined, color: Colors.lightBlue.shade300, @@ -995,8 +1001,8 @@ class _ModManagerPageState extends State { children: [ // Description tooltip if (mod.description.isNotEmpty) - MarkdownTooltip( - markdownContent: mod.description, + HtmlTooltip( + content: mod.description, child: Icon( Icons.description_outlined, color: Colors.lightBlue.shade300, diff --git a/lib/widgets/mod_card_example.dart b/lib/widgets/mod_card_example.dart new file mode 100644 index 0000000..dd5848d --- /dev/null +++ b/lib/widgets/mod_card_example.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import '../components/html_tooltip.dart'; + +class ModCardExample extends StatelessWidget { + const ModCardExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Mod Description Examples'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + _buildExampleCard( + 'HTML Description Example', + '

      My Awesome Mod

      This is a HTML description example with formatting.

      Features:

      • Feature 1
      • Feature 2
      • Feature 3

      Visit our website: example.com

      ', + 'HTML formatted mod description', + Colors.blue.shade100, + ), + const SizedBox(height: 16), + _buildExampleCard( + 'Markdown Description Example', + '# My Cool Mod\n\nThis is a **Markdown** description with _italic_ text.\n\nFeatures:\n* Feature A\n* Feature B\n* Feature C\n\nCheck out our [website](https://example.com)', + 'Markdown formatted mod description', + Colors.green.shade100, + ), + const SizedBox(height: 16), + _buildExampleCard( + 'BBCode Description Example', + '[h1]Amazing Mod[/h1]\n[b]This[/b] is a [i]BBCode[/i] description example.\n\n[b]Features:[/b]\n[list]\n[*]Feature X\n[*]Feature Y\n[*]Feature Z\n[/list]\n\nCheck out our site: [url=https://example.com]example.com[/url]', + 'BBCode formatted mod description', + Colors.orange.shade100, + ), + const SizedBox(height: 16), + _buildExampleCard( + 'Mixed Format Example', + '[h1]Mixed Format Mod[/h1]\n\n# Markdown Header\n\nThis description contains HTML, **Markdown**, and [i]BBCode[/i] all mixed together.\n\n
      • HTML List Item
      \n* Markdown List Item\n[list][*]BBCode List Item[/list]\n\nHTML Link\n[Website](https://example.com)\n[url=https://example.com]BBCode Link[/url]', + 'Description with mixed formatting', + Colors.purple.shade100, + ), + const SizedBox(height: 16), + _buildExampleCard( + 'RimWorld-Style Example', + '[h1]RimWorld Mod[/h1]\nThis mod adds several new features to enhance your RimWorld experience.\n\n[h1]Features[/h1]\n[list]\n[*]New buildings and furniture\n[*]Additional research projects\n[*]Balanced gameplay adjustments\n[/list]\n\n[h1]Compatibility[/h1]\nThis mod is compatible with:\n[list]\n[*]Core game version 1.4\n[*]Most other popular mods\n[/list]\n\n[h1]Installation[/h1]\n1. Subscribe to the mod\n2. Enable in mod menu\n3. Start a new game or load existing\n\n[h1]Credits[/h1]\nModder: YourName\nContributors: OtherPeople\n\n[h1]Links[/h1]\n[url=https://example.com/support]Support[/url] | [url=https://example.com/donate]Donate[/url]', + 'RimWorld-style mod description', + Colors.red.shade100, + ), + ], + ), + ), + ); + } + + Widget _buildExampleCard(String title, String description, String tooltip, Color color) { + return Card( + color: color, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + HtmlTooltip( + content: description, + maxWidth: 400, + maxHeight: 500, + child: const Icon( + Icons.info_outline, + size: 20, + ), + ), + ], + ), + const SizedBox(height: 8), + Text(tooltip), + const SizedBox(height: 16), + const Text('Hover over the info icon to see the formatted description') + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include 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); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..8236f57 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index ce647dc..b762151 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -118,6 +126,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_lints: dependency: "direct dev" description: @@ -139,6 +155,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -155,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + html: + dependency: transitive + description: + name: html + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + url: "https://pub.dev" + source: hosted + version: "0.15.5" http_multi_server: dependency: transitive description: @@ -227,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" logging: dependency: transitive description: @@ -307,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -448,6 +493,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + 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: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + url: "https://pub.dev" + source: hosted + version: "6.3.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + 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: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: @@ -522,4 +631,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.7.2 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index c2e9850..45c3cb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,8 @@ dependencies: intl: ^0.20.2 path: ^1.9.1 flutter_markdown: ^0.6.20 + flutter_html: ^3.0.0-beta.2 + url_launcher: ^6.3.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST