Skip to content

added the preview content functionality for the compose box #1061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 204 additions & 18 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import '../model/store.dart';
import 'autocomplete.dart';
import 'dialog.dart';
import 'store.dart';
import 'text.dart';
import 'theme.dart';

const double _inputVerticalPadding = 8;
Expand Down Expand Up @@ -363,18 +364,19 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
}
}


@override
Widget build(BuildContext context) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
final screenHeight = MediaQuery.of(context).size.height;

return InputDecorator(
decoration: const InputDecoration(),
child: ConstrainedBox(
constraints: const BoxConstraints(
constraints: BoxConstraints(
minHeight: _sendButtonSize - 2 * _inputVerticalPadding,

// TODO constrain this adaptively (i.e. not hard-coded 200)
maxHeight: 200,
maxHeight: screenHeight * 0.2,
),
child: ComposeAutocomplete(
narrow: widget.narrow,
Expand All @@ -389,9 +391,14 @@ class _ContentInputState extends State<_ContentInput> with WidgetsBindingObserve
maxLines: null,
textCapitalization: TextCapitalization.sentences,
);
}),
));
},
),
),
);
}



}

/// The content input for _StreamComposeBox.
Expand Down Expand Up @@ -985,7 +992,7 @@ class _ComposeBoxContainer extends StatelessWidget {
}
}

class _ComposeBoxLayout extends StatelessWidget {
class _ComposeBoxLayout extends StatefulWidget {
const _ComposeBoxLayout({
required this.topicInput,
required this.contentInput,
Expand All @@ -1000,17 +1007,32 @@ class _ComposeBoxLayout extends StatelessWidget {
final ComposeContentController contentController;
final FocusNode contentFocusNode;

@override
State<_ComposeBoxLayout> createState() => _ComposeBoxLayoutState();
}

class _ComposeBoxLayoutState extends State<_ComposeBoxLayout> {
bool isPreviewMode = false;

void togglePreview() {

setState(() {
isPreviewMode = !isPreviewMode;
widget.contentFocusNode.requestFocus();

});
}

@override
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
ColorScheme colorScheme = themeData.colorScheme;

final inputThemeData = themeData.copyWith(
inputDecorationTheme: InputDecorationTheme(
// Both [contentPadding] and [isDense] combine to make the layout compact.
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: _inputVerticalPadding),
horizontal: 10.0, vertical: _inputVerticalPadding),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
borderSide: BorderSide.none),
Expand All @@ -1026,25 +1048,189 @@ class _ComposeBoxLayout extends StatelessWidget {
child: Theme(
data: inputThemeData,
child: Column(children: [
if (topicInput != null) topicInput!,
if (topicInput != null) const SizedBox(height: 8),
contentInput,
]))),
if (widget.topicInput != null) widget.topicInput!,
if (widget.topicInput != null) const SizedBox(height: 8),
// Show input or preview box
isPreviewMode
? PreviewBox(
content: widget.contentController.textNormalized,
)
: widget.contentInput,
]),
),
),
const SizedBox(width: 8),
sendButton,
widget.sendButton,
]),
Theme(
data: themeData.copyWith(
iconTheme: themeData.iconTheme.copyWith(color: colorScheme.onSurfaceVariant)),
child: Row(children: [
_AttachFileButton(contentController: contentController, contentFocusNode: contentFocusNode),
_AttachMediaButton(contentController: contentController, contentFocusNode: contentFocusNode),
_AttachFromCameraButton(contentController: contentController, contentFocusNode: contentFocusNode),
])),
]));
IconButton(
icon: Icon(isPreviewMode ? Icons.visibility_off : Icons.visibility),
tooltip: isPreviewMode
? 'Switch to Edit Mode'
: 'Preview Content',
onPressed: togglePreview,
),
_AttachFileButton(
contentController: widget.contentController,
contentFocusNode: widget.contentFocusNode,
),
_AttachMediaButton(
contentController: widget.contentController,
contentFocusNode: widget.contentFocusNode,
),
_AttachFromCameraButton(
contentController: widget.contentController,
contentFocusNode: widget.contentFocusNode,
),

]),
),
]),
);
}
}



class PreviewBox extends StatelessWidget {
const PreviewBox({super.key, required this.content});

final String content;

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;

return Container(
constraints: const BoxConstraints(
minHeight: _sendButtonSize - 2 * _inputVerticalPadding,
maxHeight: 200,
),
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 10.0,
vertical: _inputVerticalPadding + 4,
),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(4.0),
),
child: SingleChildScrollView(
child: content.isEmpty
? Text(
'(No content to preview)',
style: weightVariableTextStyle(context).merge(
const TextStyle(
fontSize: 16,
height: 22 / 16,
),
),
)
: RichText(
text: _buildStyledText(content, context),
),
),
);
}

InlineSpan _buildStyledText(String text, BuildContext context) {
final boldPattern = RegExp(r'\*\*(.*?)\*\*');
final italicPattern = RegExp(r'\*(.*?)\*');
final codePattern = RegExp(r'```(.*?)```', dotAll: true);

final normalStyle = weightVariableTextStyle(context).merge(
const TextStyle(
fontSize: 16,
height: 22 / 16,
),
);
final boldStyle = normalStyle.merge(const TextStyle(fontWeight: FontWeight.bold));
final italicStyle = normalStyle.merge(const TextStyle(fontStyle: FontStyle.italic));

List<InlineSpan> spans = [];
int currentIndex = 0;

for (final match in codePattern.allMatches(text)) {
if (match.start > currentIndex) {
spans.add(TextSpan(
text: text.substring(currentIndex, match.start),
style: normalStyle,
));
}
spans.add(WidgetSpan(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: const Color.fromARGB(156, 47, 47, 47),
borderRadius: BorderRadius.circular(6.0),
border: Border.all(color: Colors.grey[500]!),
),
child: Text(
match.group(1) ?? '',
style: const TextStyle(
fontFamily: 'Courier',
fontSize: 14,
color: Colors.white,
),
),
),
));
currentIndex = match.end;
}

// Handle remaining text
String remainingText = text.substring(currentIndex);
currentIndex = 0;

// Process bold text
for (final match in boldPattern.allMatches(remainingText)) {
if (match.start > currentIndex) {
spans.add(TextSpan(
text: remainingText.substring(currentIndex, match.start),
style: normalStyle,
));
}
spans.add(TextSpan(
text: match.group(1),
style: boldStyle,
));
currentIndex = match.end;
}

// Process italic text
remainingText = remainingText.substring(currentIndex);
currentIndex = 0;

for (final match in italicPattern.allMatches(remainingText)) {
if (match.start > currentIndex) {
spans.add(TextSpan(
text: remainingText.substring(currentIndex, match.start),
style: normalStyle,
));
}
spans.add(TextSpan(
text: match.group(1),
style: italicStyle,
));
currentIndex = match.end;
}

// Add any remaining unstyled text
if (currentIndex < remainingText.length) {
spans.add(TextSpan(
text: remainingText.substring(currentIndex),
style: normalStyle,
));
}

return TextSpan(children: spans);
}
}
abstract class ComposeBoxController<T extends StatefulWidget> extends State<T> {
ComposeTopicController? get topicController;
ComposeContentController get contentController;
Expand Down