refactor(markdown): remove code block header and simplify styling

This commit is contained in:
cogwheel0
2025-12-08 00:14:36 +05:30
parent 4903b72753
commit d4797decc7
2 changed files with 190 additions and 84 deletions

View File

@@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import '../../theme/theme_extensions.dart';
class CodeBlockHeader extends StatelessWidget {
const CodeBlockHeader({
super.key,
required this.language,
required this.onCopy,
});
final String language;
final VoidCallback onCopy;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final materialTheme = Theme.of(context);
final isDark = materialTheme.brightness == Brightness.dark;
final label = language.isEmpty ? 'plaintext' : language;
// Match GitHub/Atom theme colors
final backgroundColor = isDark
? const Color(0xFF282c34) // Atom One Dark header
: const Color(0xFFf6f8fa); // GitHub light header
final textColor = isDark
? const Color(0xFF9da5b4) // Muted text for dark
: const Color(0xFF57606a); // GitHub gray for light
return Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
),
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
bottom: BorderSide(
color: theme.cardBorder.withValues(alpha: 0.15),
width: 1,
),
),
),
child: Row(
children: [
Text(
label,
style: AppTypography.codeStyle.copyWith(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onCopy,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
Icons.content_copy_rounded,
size: 16,
color: textColor,
),
),
),
),
],
),
);
}
}

View File

@@ -17,7 +17,6 @@ import 'package:conduit/l10n/app_localizations.dart';
import '../../theme/color_tokens.dart'; import '../../theme/color_tokens.dart';
import '../../theme/theme_extensions.dart'; import '../../theme/theme_extensions.dart';
import 'code_block_header.dart';
import 'package:conduit/core/network/self_signed_image_cache_manager.dart'; import 'package:conduit/core/network/self_signed_image_cache_manager.dart';
import 'package:conduit/core/network/image_header_utils.dart'; import 'package:conduit/core/network/image_header_utils.dart';
@@ -105,31 +104,46 @@ class ConduitMarkdown {
final highlightLanguage = _mapLanguage(normalizedLanguage); final highlightLanguage = _mapLanguage(normalizedLanguage);
// Use Atom One Dark for dark mode, GitHub for light mode // Use Atom One Dark for dark mode, GitHub for light mode
// These colors must match the highlight themes for visual consistency
final highlightTheme = isDark ? atomOneDarkTheme : githubTheme; final highlightTheme = isDark ? atomOneDarkTheme : githubTheme;
// Match theme colors for code block container
final codeBackground = isDark final codeBackground = isDark
? const Color(0xFF282c34) // Atom One Dark background ? const Color(0xFF282c34) // Atom One Dark
: const Color(0xFFfafbfc); // GitHub light background : const Color(0xFFF6F8FA); // GitHub light
// Derive border color from background for consistency
final borderColor = isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.1);
return Container( return Container(
margin: const EdgeInsets.symmetric(vertical: Spacing.sm), margin: const EdgeInsets.symmetric(vertical: Spacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: codeBackground, color: codeBackground,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(AppBorderRadius.sm),
border: Border.all(color: borderColor, width: BorderWidth.thin),
boxShadow: isDark
? null
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.03),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
CodeBlockHeader( _CodeBlockHeader(
language: normalizedLanguage, language: normalizedLanguage,
backgroundColor: codeBackground,
borderColor: borderColor,
isDark: isDark,
onCopy: () async { onCopy: () async {
await Clipboard.setData(ClipboardData(text: code)); await Clipboard.setData(ClipboardData(text: code));
if (!context.mounted) { if (!context.mounted) return;
return;
}
ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).hideCurrentSnackBar();
final l10n = AppLocalizations.of(context); final l10n = AppLocalizations.of(context);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -143,7 +157,10 @@ class ConduitMarkdown {
), ),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(Spacing.md), padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.sm + 4,
),
child: HighlightView( child: HighlightView(
code, code,
language: highlightLanguage, language: highlightLanguage,
@@ -151,6 +168,8 @@ class ConduitMarkdown {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
textStyle: AppTypography.codeStyle.copyWith( textStyle: AppTypography.codeStyle.copyWith(
fontFamily: AppTypography.monospaceFontFamily, fontFamily: AppTypography.monospaceFontFamily,
fontSize: 13,
height: 1.55,
), ),
), ),
), ),
@@ -486,6 +505,166 @@ class ConduitMarkdown {
} }
} }
/// Internal code block header with consistent styling.
class _CodeBlockHeader extends StatefulWidget {
const _CodeBlockHeader({
required this.language,
required this.backgroundColor,
required this.borderColor,
required this.isDark,
required this.onCopy,
});
final String language;
final Color backgroundColor;
final Color borderColor;
final bool isDark;
final VoidCallback onCopy;
@override
State<_CodeBlockHeader> createState() => _CodeBlockHeaderState();
}
class _CodeBlockHeaderState extends State<_CodeBlockHeader> {
bool _isHovering = false;
bool _isCopied = false;
void _handleCopy() {
widget.onCopy();
setState(() => _isCopied = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _isCopied = false);
});
}
@override
Widget build(BuildContext context) {
final label = widget.language.isEmpty ? 'plaintext' : widget.language;
// Colors derived from the code block theme for consistency
final labelColor = widget.isDark
? const Color(0xFF9DA5B4) // Atom One Dark muted
: const Color(0xFF57606A); // GitHub muted
final iconColor = _isHovering
? (widget.isDark ? const Color(0xFFABB2BF) : const Color(0xFF24292F))
: labelColor;
final successColor = widget.isDark
? const Color(0xFF98C379)
: const Color(0xFF1A7F37);
return Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs + 2,
),
decoration: BoxDecoration(
color: widget.backgroundColor,
border: Border(
bottom: BorderSide(
color: widget.borderColor,
width: BorderWidth.thin,
),
),
),
child: Row(
children: [
// Language icon
Icon(
_getLanguageIcon(label),
size: 14,
color: labelColor.withValues(alpha: 0.7),
),
const SizedBox(width: Spacing.xs),
// Language label
Text(
label,
style: AppTypography.codeStyle.copyWith(
color: labelColor,
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
),
const Spacer(),
// Copy button with hover effect
MouseRegion(
onEnter: (_) => setState(() => _isHovering = true),
onExit: (_) => setState(() => _isHovering = false),
child: GestureDetector(
onTap: _handleCopy,
child: AnimatedContainer(
duration: AnimationDuration.fast,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.xs,
),
decoration: BoxDecoration(
color: _isHovering
? widget.borderColor.withValues(alpha: 0.5)
: Colors.transparent,
borderRadius: BorderRadius.circular(AppBorderRadius.xs),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: AnimationDuration.fast,
child: Icon(
_isCopied
? Icons.check_rounded
: Icons.content_copy_rounded,
key: ValueKey(_isCopied),
size: 14,
color: _isCopied ? successColor : iconColor,
),
),
if (_isHovering || _isCopied) ...[
const SizedBox(width: Spacing.xs),
AnimatedOpacity(
duration: AnimationDuration.fast,
opacity: 1.0,
child: Text(
_isCopied ? 'Copied!' : 'Copy',
style: AppTypography.codeStyle.copyWith(
color: _isCopied ? successColor : iconColor,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
),
),
],
),
);
}
/// Returns an appropriate icon for the language.
IconData _getLanguageIcon(String language) {
final lower = language.toLowerCase();
return switch (lower) {
'dart' || 'flutter' => Icons.flutter_dash_rounded,
'python' || 'py' => Icons.code_rounded,
'javascript' || 'js' || 'typescript' || 'ts' => Icons.javascript_rounded,
'html' || 'css' || 'scss' => Icons.html_rounded,
'json' || 'yaml' || 'yml' => Icons.data_object_rounded,
'sql' || 'mysql' || 'postgresql' => Icons.storage_rounded,
'bash' || 'shell' || 'sh' || 'zsh' => Icons.terminal_rounded,
'markdown' || 'md' => Icons.article_rounded,
'swift' || 'kotlin' || 'java' => Icons.phone_iphone_rounded,
'rust' || 'go' || 'c' || 'cpp' || 'c++' => Icons.memory_rounded,
'docker' || 'dockerfile' => Icons.cloud_rounded,
_ => Icons.code_rounded,
};
}
}
// ChartJS diagram WebView widget // ChartJS diagram WebView widget
class ChartJsDiagram extends StatefulWidget { class ChartJsDiagram extends StatefulWidget {
const ChartJsDiagram({ const ChartJsDiagram({