diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 92c6ecd..b53fdb4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -75,6 +75,9 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -94,6 +97,7 @@ DEPENDENCIES: - stts (from `.symlinks/plugins/stts/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: @@ -137,6 +141,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -160,6 +166,7 @@ SPEC CHECKSUMS: SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d PODFILE CHECKSUM: df88575cf61e98a1a3edf2f8c887dad2c18c2079 diff --git a/lib/core/auth/api_auth_interceptor.dart b/lib/core/auth/api_auth_interceptor.dart index 5040a4e..5bb3fbf 100644 --- a/lib/core/auth/api_auth_interceptor.dart +++ b/lib/core/auth/api_auth_interceptor.dart @@ -112,17 +112,14 @@ class ApiAuthInterceptor extends Interceptor { // Add custom headers from server config (with safety checks) if (customHeaders.isNotEmpty) { customHeaders.forEach((key, value) { - // Don't override critical headers that we manage final lowerKey = key.toLowerCase(); - if (lowerKey != 'authorization' && - lowerKey != 'content-type' && - lowerKey != 'accept') { - options.headers[key] = value; - } else { + if (lowerKey == 'authorization') { DebugLogger.warning( 'Skipping reserved header override attempt: $key', ); + return; } + options.headers[key] = value; }); } diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 1976cee..0714794 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -519,18 +519,25 @@ final attachmentUploadQueueProvider = Provider((ref) { // Auth providers // Auth token integration with API service - using unified auth system final apiTokenUpdaterProvider = Provider((ref) { - // Listen to unified auth token changes and update API service + void syncToken(ApiService? api, String? token) { + if (api == null) return; + api.updateAuthToken(token != null && token.isNotEmpty ? token : null); + final length = token?.length ?? 0; + DebugLogger.auth( + 'token-updated', + scope: 'auth/api', + data: {'length': length}, + ); + } + + syncToken(ref.read(apiServiceProvider), ref.read(authTokenProvider3)); + + ref.listen(apiServiceProvider, (previous, next) { + syncToken(next, ref.read(authTokenProvider3)); + }); + ref.listen(authTokenProvider3, (previous, next) { - final api = ref.read(apiServiceProvider); - if (api != null) { - api.updateAuthToken(next); - final length = next?.length ?? 0; - DebugLogger.auth( - 'token-updated', - scope: 'auth/api', - data: {'length': length}, - ); - } + syncToken(ref.read(apiServiceProvider), next); }); }); diff --git a/lib/features/auth/providers/unified_auth_providers.dart b/lib/features/auth/providers/unified_auth_providers.dart index 0cced98..29e9abe 100644 --- a/lib/features/auth/providers/unified_auth_providers.dart +++ b/lib/features/auth/providers/unified_auth_providers.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/auth/auth_state_manager.dart'; import '../../../core/models/user.dart'; import '../../../core/providers/app_providers.dart'; +import '../../../core/services/api_service.dart'; /// Unified auth providers using the new auth state manager /// These replace the old auth providers for better efficiency @@ -107,11 +108,24 @@ final authStatusProvider = Provider((ref) { /// Provider to watch for auth state changes and update API service final authApiIntegrationProvider = Provider((ref) { - ref.listen(authTokenProvider3, (previous, next) { - final api = ref.read(apiServiceProvider); - if (api != null && next != null && next.isNotEmpty) { - api.updateAuthToken(next); + void syncToken(ApiService? api, String? token) { + if (api == null) return; + if (token == null || token.isEmpty) { + api.updateAuthToken(null); + return; } + api.updateAuthToken(token); + } + + // Ensure the current ApiService instance immediately picks up the cached token. + syncToken(ref.read(apiServiceProvider), ref.read(authTokenProvider3)); + + ref.listen(apiServiceProvider, (previous, next) { + syncToken(next, ref.read(authTokenProvider3)); + }); + + ref.listen(authTokenProvider3, (previous, next) { + syncToken(ref.read(apiServiceProvider), next); }); }); diff --git a/lib/shared/widgets/markdown/markdown_config.dart b/lib/shared/widgets/markdown/markdown_config.dart index 19b9964..88a7f65 100644 --- a/lib/shared/widgets/markdown/markdown_config.dart +++ b/lib/shared/widgets/markdown/markdown_config.dart @@ -2,34 +2,57 @@ import 'dart:async'; import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_highlight/themes/a11y-dark.dart'; +import 'package:flutter_highlight/themes/a11y-light.dart'; +import 'package:markdown/markdown.dart' as m; +import 'package:markdown_widget/markdown_widget.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import '../../theme/theme_extensions.dart'; import '../../theme/color_tokens.dart'; +import '../../theme/theme_extensions.dart'; +import 'markdown_latex.dart'; +/// Callback invoked when a markdown link is tapped. +typedef MarkdownLinkTapCallback = void Function(String url, String title); + +/// Bundles markdown configuration and generator metadata for the app theme. class ConduitMarkdownTheme { const ConduitMarkdownTheme({ - required this.styleSheet, - required this.imageBuilder, - required this.linkColor, - required this.linkHoverColor, + required this.config, + required this.inlineSyntaxes, + required this.blockSyntaxes, + required this.generators, + required this.linesMargin, }); - final MarkdownStyleSheet styleSheet; - final MarkdownImageBuilder imageBuilder; - final Color linkColor; - final Color linkHoverColor; + final MarkdownConfig config; + final List inlineSyntaxes; + final List blockSyntaxes; + final List generators; + final EdgeInsets linesMargin; + + MarkdownGenerator createGenerator() { + return MarkdownGenerator( + inlineSyntaxList: inlineSyntaxes, + blockSyntaxList: blockSyntaxes, + linesMargin: linesMargin, + generators: generators, + ); + } } class ConduitMarkdownConfig { - static ConduitMarkdownTheme resolve(BuildContext context) { + static ConduitMarkdownTheme resolve( + BuildContext context, { + MarkdownLinkTapCallback? onTapLink, + }) { final theme = context.conduitTheme; final materialTheme = Theme.of(context); + final isDark = materialTheme.brightness == Brightness.dark; final baseBody = AppTypography.bodyMediumStyle.copyWith( color: theme.textPrimary, @@ -42,83 +65,153 @@ class ConduitMarkdownConfig { final codeBackground = theme.surfaceContainer.withValues(alpha: 0.55); final borderColor = theme.cardBorder.withValues(alpha: 0.25); + final latex = const ConduitLatex(); - final styleSheet = MarkdownStyleSheet( - a: baseBody.copyWith( - color: materialTheme.colorScheme.primary, - decoration: TextDecoration.underline, - decorationColor: materialTheme.colorScheme.primary, - ), - p: baseBody, - blockSpacing: Spacing.sm, - listIndent: Spacing.lg, - listBullet: baseBody.copyWith(color: theme.textSecondary), - listBulletPadding: const EdgeInsets.only(right: Spacing.xs), - checkbox: baseBody.copyWith(color: theme.textSecondary), - em: baseBody.copyWith(fontStyle: FontStyle.italic), - strong: baseBody.copyWith(fontWeight: FontWeight.w600), - del: baseBody.copyWith(decoration: TextDecoration.lineThrough), - h1: AppTypography.headlineLargeStyle.copyWith(color: theme.textPrimary), - h2: AppTypography.headlineMediumStyle.copyWith(color: theme.textPrimary), - h3: AppTypography.headlineSmallStyle.copyWith(color: theme.textPrimary), - h4: AppTypography.bodyLargeStyle.copyWith(color: theme.textPrimary), - h5: baseBody.copyWith(fontWeight: FontWeight.w600), - h6: secondaryBody, - blockquote: baseBody.copyWith(color: theme.textSecondary), - blockquotePadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - blockquoteDecoration: BoxDecoration( - color: theme.surfaceContainer.withValues(alpha: 0.35), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - border: Border( - left: BorderSide( - width: BorderWidth.standard, - color: materialTheme.colorScheme.primary.withValues(alpha: 0.35), - ), - ), - ), - code: AppTypography.codeStyle.copyWith( - color: theme.codeText, - backgroundColor: codeBackground, - ), - codeblockPadding: const EdgeInsets.all(Spacing.sm), - codeblockDecoration: BoxDecoration( - color: codeBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - border: Border.all(color: borderColor, width: BorderWidth.micro), - ), - horizontalRuleDecoration: BoxDecoration( - border: Border( - top: BorderSide(color: theme.dividerColor, width: BorderWidth.small), - ), - ), - tableHead: secondaryBody.copyWith(fontWeight: FontWeight.w600), - tableBody: secondaryBody, - tableBorder: TableBorder.all( - color: borderColor, - width: BorderWidth.micro, - ), - tableCellsPadding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), - tableCellsDecoration: BoxDecoration( - color: theme.surfaceBackground.withValues(alpha: 0.35), - ), - tableHeadAlign: TextAlign.left, - tablePadding: const EdgeInsets.only(bottom: Spacing.xs), - ); + final markdownConfig = + (isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig) + .copy( + configs: [ + PConfig(textStyle: baseBody), + H1Config( + style: AppTypography.headlineLargeStyle.copyWith( + color: theme.textPrimary, + ), + ), + H2Config( + style: AppTypography.headlineMediumStyle.copyWith( + color: theme.textPrimary, + ), + ), + H3Config( + style: AppTypography.headlineSmallStyle.copyWith( + color: theme.textPrimary, + ), + ), + H4Config( + style: AppTypography.bodyLargeStyle.copyWith( + color: theme.textPrimary, + ), + ), + H5Config(style: baseBody.copyWith(fontWeight: FontWeight.w600)), + H6Config(style: secondaryBody), + LinkConfig( + style: baseBody.copyWith( + color: materialTheme.colorScheme.primary, + decoration: TextDecoration.underline, + decorationColor: materialTheme.colorScheme.primary, + ), + onTap: (url) => onTapLink?.call(url, url), + ), + CodeConfig( + style: AppTypography.codeStyle.copyWith( + color: theme.codeText, + backgroundColor: codeBackground, + ), + ), + PreConfig( + padding: const EdgeInsets.all(Spacing.sm), + margin: const EdgeInsets.symmetric(vertical: Spacing.xs), + decoration: BoxDecoration( + color: codeBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.sm), + border: Border.all( + color: borderColor, + width: BorderWidth.micro, + ), + ), + textStyle: AppTypography.codeStyle.copyWith( + color: theme.codeText, + ), + styleNotMatched: AppTypography.codeStyle.copyWith( + color: theme.codeText, + ), + theme: _codeHighlightTheme(theme, isDark: isDark), + language: 'plaintext', + ), + BlockquoteConfig( + sideColor: materialTheme.colorScheme.primary.withValues( + alpha: 0.35, + ), + textColor: theme.textSecondary, + sideWith: BorderWidth.micro, + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + margin: const EdgeInsets.symmetric(vertical: Spacing.sm), + ), + ListConfig(marginLeft: Spacing.lg, marginBottom: Spacing.xs), + TableConfig( + border: TableBorder.all( + color: borderColor, + width: BorderWidth.micro, + ), + headPadding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + bodyPadding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + headerStyle: secondaryBody.copyWith( + fontWeight: FontWeight.w600, + ), + bodyStyle: secondaryBody, + headerRowDecoration: BoxDecoration( + color: theme.surfaceBackground.withValues(alpha: 0.35), + ), + bodyRowDecoration: BoxDecoration( + color: theme.surfaceContainer.withValues(alpha: 0.2), + ), + ), + HrConfig(color: theme.dividerColor, height: BorderWidth.small), + ImgConfig( + builder: (url, _) { + return Builder( + builder: (context) { + final uri = Uri.tryParse(url); + if (uri == null) { + return _buildImageError( + context, + context.conduitTheme, + ); + } + return _buildImage(context, uri); + }, + ); + }, + ), + ], + ); return ConduitMarkdownTheme( - styleSheet: styleSheet, - imageBuilder: (uri, title, alt) => _buildImage(context, uri), - linkColor: materialTheme.colorScheme.primary, - linkHoverColor: materialTheme.colorScheme.primary.withValues(alpha: 0.8), + config: markdownConfig, + inlineSyntaxes: [latex.syntax()], + blockSyntaxes: const [], + generators: [latex.generator(isDark: isDark)], + linesMargin: const EdgeInsets.only(bottom: Spacing.sm), ); } + static Map _codeHighlightTheme( + ConduitThemeExtension theme, { + required bool isDark, + }) { + final baseTheme = isDark ? a11yDarkTheme : a11yLightTheme; + final codeStyle = AppTypography.codeStyle.copyWith(color: theme.codeText); + + return { + for (final entry in baseTheme.entries) + entry.key: entry.value.copyWith( + color: entry.value.color ?? theme.codeText, + fontFamily: AppTypography.monospaceFontFamily, + fontSize: codeStyle.fontSize, + height: codeStyle.height, + ), + }; + } + static Widget buildMermaidBlock(BuildContext context, String code) { final conduitTheme = context.conduitTheme; final materialTheme = Theme.of(context); diff --git a/lib/shared/widgets/markdown/markdown_latex.dart b/lib/shared/widgets/markdown/markdown_latex.dart new file mode 100644 index 0000000..a04debc --- /dev/null +++ b/lib/shared/widgets/markdown/markdown_latex.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:markdown/markdown.dart' as m; +import 'package:markdown_widget/markdown_widget.dart'; + +import '../../theme/theme_extensions.dart'; + +const String _latexTag = 'latex'; + +/// Provides LaTeX parsing support for markdown_widget. +class ConduitLatex { + const ConduitLatex(); + + /// Returns the inline syntax used to identify LaTeX segments. + m.InlineSyntax syntax() => _LatexSyntax(); + + /// Returns the span generator that renders LaTeX expressions. + SpanNodeGeneratorWithTag generator({required bool isDark}) { + return SpanNodeGeneratorWithTag( + tag: _latexTag, + generator: (element, config, visitor) { + return _LatexNode( + attributes: element.attributes, + rawText: element.textContent, + config: config, + isDark: isDark, + ); + }, + ); + } +} + +class _LatexSyntax extends m.InlineSyntax { + _LatexSyntax() : super(r'(\$\$[\s\S]+?\$\$)|(\$[^\n]+?\$)'); + + @override + bool onMatch(m.InlineParser parser, Match match) { + final raw = match.input.substring(match.start, match.end); + final element = m.Element.text(_latexTag, raw); + if (raw.startsWith(r'$$') && raw.endsWith(r'$$') && raw.length > 4) { + element.attributes['content'] = raw.substring(2, raw.length - 2); + element.attributes['isInline'] = 'false'; + } else if (raw.startsWith(r'$') && raw.endsWith(r'$') && raw.length > 2) { + element.attributes['content'] = raw.substring(1, raw.length - 1); + element.attributes['isInline'] = 'true'; + } else { + element.attributes['content'] = raw; + element.attributes['isInline'] = 'true'; + } + parser.addNode(element); + return true; + } +} + +class _LatexNode extends SpanNode { + _LatexNode({ + required this.attributes, + required this.rawText, + required this.config, + required this.isDark, + }); + + final Map attributes; + final String rawText; + final MarkdownConfig config; + final bool isDark; + + @override + InlineSpan build() { + final content = attributes['content']?.trim(); + final isInline = attributes['isInline'] == 'true'; + final baseStyle = (parentStyle ?? config.p.textStyle).copyWith( + color: + (parentStyle ?? config.p.textStyle).color ?? + (isDark ? Colors.white : Colors.black), + ); + + if (content == null || content.isEmpty) { + return TextSpan(text: rawText, style: baseStyle); + } + + final latexWidget = Math.tex( + content, + mathStyle: MathStyle.text, + textStyle: baseStyle, + textScaleFactor: 1, + onErrorFallback: (error) { + return Text(rawText, style: baseStyle.copyWith(color: Colors.red)); + }, + ); + + final widget = isInline + ? latexWidget + : Padding( + padding: const EdgeInsets.symmetric(vertical: Spacing.xs), + child: Center(child: latexWidget), + ); + + return WidgetSpan(alignment: PlaceholderAlignment.middle, child: widget); + } +} diff --git a/lib/shared/widgets/markdown/streaming_markdown_widget.dart b/lib/shared/widgets/markdown/streaming_markdown_widget.dart index 106ba76..aa5b50c 100644 --- a/lib/shared/widgets/markdown/streaming_markdown_widget.dart +++ b/lib/shared/widgets/markdown/streaming_markdown_widget.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import '../../theme/theme_extensions.dart'; import 'markdown_config.dart'; import 'markdown_preprocessor.dart'; -typedef MarkdownLinkTapCallback = void Function(String url, String title); - class StreamingMarkdownWidget extends StatelessWidget { const StreamingMarkdownWidget({ super.key, @@ -26,26 +23,20 @@ class StreamingMarkdownWidget extends StatelessWidget { } final normalized = ConduitMarkdownPreprocessor.normalize(content); - final markdownTheme = ConduitMarkdownConfig.resolve(context); + final markdownTheme = ConduitMarkdownConfig.resolve( + context, + onTapLink: onTapLink, + ); + final generator = markdownTheme.createGenerator(); final mermaidRegex = RegExp(r'```mermaid\s*([\s\S]*?)```', multiLine: true); final matches = mermaidRegex.allMatches(normalized).toList(); - Widget buildMarkdown(String data) => MarkdownBody( - data: data, - styleSheet: markdownTheme.styleSheet, - selectable: false, - imageBuilder: markdownTheme.imageBuilder, - onTapLink: (text, href, title) { - final target = href ?? ''; - if (target.isEmpty) { - return; - } - final resolvedTitle = title.isNotEmpty ? title : text; - onTapLink?.call(target, resolvedTitle); - }, - ); + List buildMarkdownBlocks(String data) { + return generator.buildWidgets(data, config: markdownTheme.config); + } if (matches.isEmpty) { + final blocks = buildMarkdownBlocks(normalized); return SelectionArea( child: Theme( data: Theme.of(context).copyWith( @@ -53,7 +44,10 @@ class StreamingMarkdownWidget extends StatelessWidget { cursorColor: context.conduitTheme.buttonPrimary, ), ), - child: buildMarkdown(normalized), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: blocks, + ), ), ); } @@ -63,7 +57,7 @@ class StreamingMarkdownWidget extends StatelessWidget { for (final match in matches) { final before = normalized.substring(currentIndex, match.start); if (before.trim().isNotEmpty) { - children.add(buildMarkdown(before)); + children.addAll(buildMarkdownBlocks(before)); } final code = match.group(1)?.trim() ?? ''; @@ -76,7 +70,7 @@ class StreamingMarkdownWidget extends StatelessWidget { final tail = normalized.substring(currentIndex); if (tail.trim().isNotEmpty) { - children.add(buildMarkdown(tail)); + children.addAll(buildMarkdownBlocks(tail)); } return SelectionArea( diff --git a/pubspec.lock b/pubspec.lock index 4d3f17f..fe442a8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -451,14 +451,14 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_markdown_plus: + flutter_math_fork: dependency: "direct main" description: - name: flutter_markdown_plus - sha256: "7f349c075157816da399216a4127096108fd08e1ac931e34e72899281db4113c" + name: flutter_math_fork + sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "0.7.4" flutter_native_splash: dependency: "direct dev" description: @@ -539,6 +539,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678 + url: "https://pub.dev" + source: hosted + version: "2.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -830,13 +838,21 @@ packages: source: hosted version: "1.3.0" markdown: - dependency: transitive + dependency: "direct main" description: name: markdown sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted version: "7.3.0" + markdown_widget: + dependency: "direct main" + description: + name: markdown_widget + sha256: b52c13d3ee4d0e60c812e15b0593f142a3b8a2003cde1babb271d001a1dbdc1c + url: "https://pub.dev" + source: hosted + version: "2.3.2+8" matcher: dependency: transitive description: @@ -877,6 +893,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" node_preamble: dependency: transitive description: @@ -925,6 +949,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1013,6 +1045,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.3" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1141,6 +1181,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" share_handler: dependency: "direct main" description: @@ -1498,6 +1546,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: @@ -1586,6 +1642,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" vector_math: dependency: transitive description: @@ -1594,6 +1674,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 713e09e..77a05aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,6 @@ dependencies: shared_preferences: ^2.3.2 # UI Components - GPT Markdown - flutter_markdown_plus: ^1.0.3 cached_network_image: ^3.3.1 flutter_highlight: ^0.7.0 webview_flutter: ^4.7.0 @@ -66,6 +65,9 @@ dependencies: share_plus: ^12.0.0 share_handler: ^0.0.19 riverpod_annotation: ^3.0.0 + markdown_widget: ^2.3.2+8 + flutter_math_fork: ^0.7.4 + markdown: ^7.3.0 # Clipboard functionality is available through flutter/services (part of Flutter SDK)