Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Created November 1, 2023 22:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rodydavis/e1cf79e38fc438625a202491870d2b71 to your computer and use it in GitHub Desktop.
Save rodydavis/e1cf79e38fc438625a202491870d2b71 to your computer and use it in GitHub Desktop.
Flutter input, output, preview
import 'package:flutter/material.dart';
import 'two_pane.dart';
class InputOutputPreview extends StatefulWidget {
const InputOutputPreview({
super.key,
required this.title,
required this.input,
required this.output,
required this.preview,
required this.placeholder,
this.actions = const [],
this.codeTitle = 'Code',
this.previewTitle = 'Preview',
this.loading = false,
this.lazy = false,
this.previewSize = const Size(300, 700),
});
final (
String,
ValueChanged<(TextEditingController, TextEditingController)>
) input, output;
final Widget? preview;
final Widget placeholder;
final String title;
final List<Widget> actions;
final String codeTitle, previewTitle;
final Size? previewSize;
final bool loading;
final bool lazy;
@override
State<InputOutputPreview> createState() => _InputOutputPreviewState();
}
class _InputOutputPreviewState extends State<InputOutputPreview> {
final input = TextEditingController();
final output = TextEditingController();
String? lastInput;
String? lastOutput;
@override
void initState() {
super.initState();
if (!widget.lazy) input.addListener(onInput);
output.addListener(onOutput);
}
@override
void dispose() {
super.dispose();
if (!widget.lazy) input.removeListener(onInput);
output.removeListener(onOutput);
input.dispose();
output.dispose();
}
void onInput() {
final (_, update) = widget.input;
final str = input.text;
if (lastInput == str) return;
update((input, output));
lastInput = str;
}
void onOutput() {
final (_, update) = widget.output;
final str = output.text;
if (lastOutput == str) return;
update((output, input));
lastOutput = str;
}
@override
Widget build(BuildContext context) {
final (inputTitle, _) = widget.input;
final (outputTitle, _) = widget.output;
return TwoPane(
title: widget.title,
actions: widget.actions,
loading: widget.loading,
primary: (
widget.codeTitle,
(context) => SizedBox(
height: double.infinity,
child: Column(
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.all(8),
child: Card(
child: ListTile(
title: Text(inputTitle),
subtitle: TextField(
maxLines: null,
controller: input,
expands: true,
decoration: InputDecoration(
isCollapsed: true,
border: InputBorder.none,
suffix: widget.lazy
? IconButton(
onPressed: onInput,
icon: const Icon(Icons.save),
tooltip: 'Submit',
)
: null,
),
),
),
),
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8),
child: Card(
child: ListTile(
title: Text(outputTitle),
subtitle: TextField(
maxLines: null,
controller: output,
expands: true,
decoration: const InputDecoration(
isCollapsed: true,
border: InputBorder.none,
),
),
),
),
),
),
],
),
),
),
secondary: (
widget.previewTitle,
(context) => Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Builder(builder: (context) {
if (widget.previewSize == null) {
return widget.preview ?? widget.placeholder;
}
return Center(
child: Material(
elevation: 8,
child: SizedBox.fromSize(
size: widget.previewSize,
child: widget.preview ?? widget.placeholder,
),
),
);
}),
)
),
);
}
}
import 'package:flutter/material.dart';
class TwoPane extends StatefulWidget {
const TwoPane({
super.key,
required this.primary,
required this.secondary,
required this.title,
this.actions = const [],
this.loading = false,
});
final (String, WidgetBuilder) primary, secondary;
final List<Widget> actions;
final String title;
final bool loading;
@override
State<TwoPane> createState() => _TwoPaneState();
}
class _TwoPaneState extends State<TwoPane> {
bool darkMode = false;
void toggleDarkMode() {
if (mounted) {
setState(() {
darkMode = !darkMode;
});
}
}
ThemeData theme(Color color, Brightness brightness) {
return ThemeData(
brightness: brightness,
colorScheme: ColorScheme.fromSeed(
seedColor: color,
brightness: brightness,
),
useMaterial3: true,
);
}
@override
void didUpdateWidget(covariant TwoPane oldWidget) {
if (oldWidget.loading != widget.loading ||
oldWidget.title != widget.title ||
oldWidget.actions != widget.actions) {
if (mounted) setState(() {});
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return Theme(
data: theme(Colors.purple, darkMode ? Brightness.dark : Brightness.light),
child: Builder(builder: (context) {
final (primaryTitle, primaryBuilder) = widget.primary;
final (secondaryTitle, secondaryBuilder) = widget.secondary;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: false,
actions: [
IconButton(
tooltip: 'Toggle dark mode',
onPressed: toggleDarkMode,
icon: Icon(darkMode ? Icons.light_mode : Icons.dark_mode),
),
...widget.actions,
],
),
body: LayoutBuilder(
builder: (context, dimens) {
if (dimens.maxWidth > 800 && dimens.maxHeight > 600) {
return Column(
children: [
if (widget.loading) const LinearProgressIndicator(),
Expanded(
child: Row(
children: [
Flexible(
flex: 1,
child: primaryBuilder(context),
),
Flexible(
flex: 1,
child: secondaryBuilder(context),
),
],
),
),
],
);
}
return DefaultTabController(
length: 2,
child: Column(
children: [
SizedBox(
height: kToolbarHeight,
width: double.infinity,
child: TabBar(
tabs: [
Tab(text: primaryTitle),
Tab(text: secondaryTitle),
],
),
),
if (widget.loading) const LinearProgressIndicator(),
Expanded(
child: TabBarView(
children: [
primaryBuilder(context),
secondaryBuilder(context),
],
),
),
],
),
);
},
));
}),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment