Files
packages.dart/deprecated/storyboard/lib/src/storyboard.dart
T
2026-01-15 14:38:46 -08:00

651 lines
20 KiB
Dart

library storyboard;
import 'package:device_frame/device_frame.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'custom_rect_clipper.dart';
import 'media_query_observer.dart';
import 'nested_app.dart';
import 'rounded_label.dart';
part 'render_lane.dart';
const _kSpacing = 40.0;
const _kLabelHeight = 65.0;
const _kCompactBreakpoint = 400.0;
const double _MAXSIZE = 100000;
const double _MINSCALE = 0.0001;
const double _STARTSCALE = 0.35;
const _kTitle = 'Storyboard';
typedef ScreenBuilder = Widget Function(
BuildContext context, RouteSettings route, Widget child);
class StoryBoard extends StatefulWidget {
const StoryBoard({
Key? key,
this.children,
this.customLanes,
this.sizedChildren,
bool showAppBar = false,
Size? childSize,
this.initialOffset = Offset.zero,
this.initialScale = _STARTSCALE,
this.customAppBar,
this.crossAxisCount,
this.title = _kTitle,
this.laneBuilder,
this.childrenLabel = 'Children',
this.sizedChildrenLabel = 'Sized Children',
this.orientation,
this.androidDevice,
this.cupertinoDevice,
this.deviceFrameStyle,
}) : _materialApp = null,
_widgetsApp = null,
_cupertinoApp = null,
_widgets = true,
screenSize = childSize,
customRoutes = null,
hideAppBar = !showAppBar,
enabled = true,
assert(
children != null || customLanes != null || sizedChildren != null),
super(key: key);
const StoryBoard.app({
Key? key,
MaterialApp? materialApp,
WidgetsApp? widgetsApp,
CupertinoApp? cupertinoApp,
List<Widget>? customScreens,
this.enabled = true,
this.screenSize = const Size(400, 700),
this.initialOffset = Offset.zero,
this.initialScale = _STARTSCALE,
this.customLanes,
this.customAppBar,
this.customRoutes,
this.crossAxisCount,
this.title = _kTitle,
this.laneBuilder,
this.childrenLabel = 'Children',
this.sizedChildrenLabel = 'Sized Children',
this.sizedChildren,
this.orientation,
this.androidDevice,
this.cupertinoDevice,
this.deviceFrameStyle,
}) : _materialApp = materialApp,
_widgetsApp = widgetsApp,
_cupertinoApp = cupertinoApp,
children = customScreens,
_widgets = false,
hideAppBar = false,
assert(
materialApp != null || widgetsApp != null || cupertinoApp != null),
super(key: key);
const StoryBoard.cupertino({
Key? key,
required CupertinoApp child,
List<Widget>? customScreens,
this.enabled = true,
this.screenSize = const Size(400, 700),
this.initialOffset = Offset.zero,
this.initialScale = _STARTSCALE,
this.customLanes,
this.customAppBar,
this.customRoutes,
this.crossAxisCount,
this.title = _kTitle,
this.laneBuilder,
this.sizedChildren,
this.childrenLabel = 'Children',
this.sizedChildrenLabel = 'Sized Children',
this.orientation,
this.androidDevice,
this.cupertinoDevice,
this.deviceFrameStyle,
}) : _materialApp = null,
_widgetsApp = null,
_cupertinoApp = child,
children = customScreens,
_widgets = false,
hideAppBar = false,
super(key: key);
const StoryBoard.widgets({
Key? key,
required WidgetsApp child,
List<Widget>? customScreens,
this.enabled = true,
this.screenSize = const Size(400, 700),
this.initialOffset = Offset.zero,
this.initialScale = _STARTSCALE,
this.customLanes,
this.customAppBar,
this.customRoutes,
this.crossAxisCount,
this.title = _kTitle,
this.laneBuilder,
this.sizedChildren,
this.childrenLabel = 'Children',
this.sizedChildrenLabel = 'Sized Children',
this.orientation,
this.androidDevice,
this.cupertinoDevice,
this.deviceFrameStyle,
}) : _materialApp = null,
_widgetsApp = child,
_cupertinoApp = null,
children = customScreens,
_widgets = false,
hideAppBar = false,
super(key: key);
const StoryBoard.material({
Key? key,
required MaterialApp child,
List<Widget>? customScreens,
this.enabled = true,
this.screenSize = const Size(400, 700),
this.initialOffset = Offset.zero,
this.initialScale = _STARTSCALE,
this.customLanes,
this.customAppBar,
this.customRoutes,
this.crossAxisCount,
this.title = _kTitle,
this.laneBuilder,
this.sizedChildren,
this.childrenLabel = 'Children',
this.sizedChildrenLabel = 'Sized Children',
this.orientation,
this.androidDevice,
this.cupertinoDevice,
this.deviceFrameStyle,
}) : _materialApp = child,
_widgetsApp = null,
_cupertinoApp = null,
children = customScreens,
_widgets = false,
hideAppBar = false,
super(key: key);
/// Override the default app bar of the storyboard
final PreferredSizeWidget? customAppBar;
/// Custom routes you want to show with test data
final List<RouteSettings>? customRoutes;
/// Custom screens you want to show
final List<Widget>? children;
/// Children that can have custom sizes
final List<CustomScreen>? sizedChildren;
/// List of custom lanes
final List<CustomLane>? customLanes;
/// You can disable this widget at any time and just return the child
final bool enabled;
/// Initial Offset of the canvas
final Offset initialOffset;
/// Initial Scale of the canvas
final double initialScale;
/// Size for each screen
final Size? screenSize;
/// Wrap your Cupertino App with this widget
final CupertinoApp? _cupertinoApp;
/// Wrap your Material App with this widget
final MaterialApp? _materialApp;
/// Wrap your Widgets App with this widget
final WidgetsApp? _widgetsApp;
/// Number of widgets to show in a row instead of maxLaneWidth
final int? crossAxisCount;
/// Optionally hide the AppBar if used as a widget
final bool hideAppBar;
/// Title for AppBar
final String title;
/// Optionally wrap the lane building process to customize the look
final LaneBuilder? laneBuilder;
/// Label for Custom Screens or Children
final String? childrenLabel, sizedChildrenLabel;
/// Use a cupertino device for every screen and ignore the size
final DeviceInfo? cupertinoDevice;
/// Use a android device for every screen and ignore the size
final DeviceInfo? androidDevice;
/// Device orientation for every screen and ignore the size
final Orientation? orientation;
/// Device Frame style
final DeviceFrameStyle? deviceFrameStyle;
final bool _widgets;
@override
StoryboardController createState() => StoryboardController();
}
class StoryboardController extends State<StoryBoard> {
final TransformationController _controller = TransformationController();
late FocusNode _focusNode;
Offset _offset = Offset.zero;
double _scale = 1;
UniqueKey _key = UniqueKey();
@override
void didUpdateWidget(StoryBoard oldWidget) {
if (oldWidget._widgetsApp != widget._widgetsApp) {
if (mounted) setState(() {});
}
if (oldWidget._materialApp != widget._materialApp) {
if (mounted) setState(() {});
}
if (oldWidget._cupertinoApp != widget._cupertinoApp) {
if (mounted) setState(() {});
}
if (oldWidget.cupertinoDevice != widget.cupertinoDevice) {
if (mounted) setState(() {});
}
if (oldWidget.androidDevice != widget.androidDevice) {
if (mounted) setState(() {});
}
if (oldWidget.orientation != widget.orientation) {
if (mounted) setState(() {});
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
void initState() {
_focusNode = FocusNode();
_controller.value.scale(widget.initialScale);
final _offset = widget.initialOffset;
_controller.value.translate(_offset.dx, _offset.dy);
super.initState();
}
void restart() {
_key = UniqueKey();
if (mounted) setState(() {});
}
Widget get app {
if (materialApp != null) return materialApp!;
if (widgetsApp != null) return widgetsApp!;
if (cupertinoApp != null) return cupertinoApp!;
return Container();
}
MaterialApp? get materialApp => widget._materialApp;
WidgetsApp? get widgetsApp => widget._widgetsApp;
CupertinoApp? get cupertinoApp => widget._cupertinoApp;
Widget? get home {
if (materialApp != null) return materialApp?.home;
if (widgetsApp != null) return widgetsApp?.home;
if (cupertinoApp != null) return cupertinoApp?.home;
return null;
}
Map<String, WidgetBuilder>? get routes {
if (materialApp != null) return materialApp?.routes;
if (widgetsApp != null) return widgetsApp?.routes;
if (cupertinoApp != null) return cupertinoApp?.routes;
return null;
}
String? get initialRoute {
if (materialApp != null) return materialApp?.initialRoute;
if (widgetsApp != null) return widgetsApp?.initialRoute;
if (cupertinoApp != null) return cupertinoApp?.initialRoute;
return null;
}
Size? get size {
if (widget.screenSize != null) {
return Size(
widget.screenSize!.width,
widget.screenSize!.height + _kLabelHeight,
);
}
return null;
}
CustomScreen _addChild(
RouteSettings? route, {
Widget? child,
String? label,
bool customWidget = false,
Size? customSize,
}) {
final Size? _size = customSize ?? widget.screenSize ?? size;
return CustomScreen(
size: size,
child: _buildApp(_size, route, child, customWidget),
);
}
Widget _buildApp(
Size? _size,
RouteSettings? route,
Widget? child,
bool customWidget,
) {
return Material(
elevation: 2,
child: SizedBox.fromSize(
size: _size,
child: ClipRect(
clipper: CustomRect(const Offset(0, 0)),
child: NestedApp(
route: route,
size: _size,
customWidget: customWidget,
materialApp: materialApp,
cupertinoApp: cupertinoApp,
widgetsApp: widgetsApp,
child: child,
),
),
),
);
}
@override
Widget build(BuildContext context) {
if (!widget.enabled) return app;
// ignore: unnecessary_nullable_for_final_variable_declarations
final ThemeData? _theme = Theme.of(context);
return MaterialApp(
key: _key,
debugShowCheckedModeBanner: false,
themeMode: materialApp?.themeMode ?? ThemeMode.system,
theme: materialApp?.theme ?? _theme ?? ThemeData.light(),
darkTheme: materialApp?.darkTheme ?? _theme ?? ThemeData.dark(),
// builder: (context, child) => MediaQueryObserver(
// data: MediaQuery.of(context).copyWith(size: size),
// child: child,
// ),
home: Builder(
builder: (context) => DeviceFrameTheme(
// style: Theme.of(context).brightness == Brightness.dark
// ? DeviceFrameStyle.dark()
// : DeviceFrameStyle.light(),
style: widget.deviceFrameStyle ?? DeviceFrameStyle.dark(),
child: LayoutBuilder(
builder: (context, dimens) => Scaffold(
appBar: _buildAppBar(dimens.maxWidth <= _kCompactBreakpoint),
body: InteractiveViewer(
transformationController: _controller,
constrained: false,
minScale: _MINSCALE,
child: SizedBox(
width: _MAXSIZE,
height: _MAXSIZE,
child: renderView(),
),
),
),
),
),
),
);
}
Widget renderView() {
return Stack(
fit: StackFit.expand,
// overflow: Overflow.visible,
clipBehavior: Clip.none,
children: [
if (widget._widgets)
Positioned(
top: _offset.dy,
left: _offset.dx,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.children != null)
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
laneBuilder: widget.laneBuilder,
title: widget.childrenLabel ?? '',
scale: _scale,
size: size,
children: widget.children!
.map(
(e) => CustomScreen(
size: size,
child: e,
),
)
.toList(),
crossAxisCount: widget.crossAxisCount,
shadow: const BoxShadow(
color: Colors.black45,
offset: Offset(2, 2),
blurRadius: 10,
spreadRadius: 2,
),
),
if (widget.sizedChildren != null)
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
laneBuilder: widget.laneBuilder,
title: widget.sizedChildrenLabel ?? '',
scale: _scale,
size: size,
children: widget.sizedChildren!,
crossAxisCount: widget.crossAxisCount,
shadow: const BoxShadow(
color: Colors.black45,
offset: Offset(2, 2),
blurRadius: 10,
spreadRadius: 2,
),
),
if (widget.customLanes != null)
for (final lane in widget.customLanes!)
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
laneBuilder: widget.laneBuilder,
title: lane.title,
scale: _scale,
size: size,
crossAxisCount: widget.crossAxisCount,
children: lane.children,
shadow: const BoxShadow(
color: Colors.black45,
offset: Offset(2, 2),
blurRadius: 10,
spreadRadius: 2,
),
),
],
),
),
if (!widget._widgets) ...[
Positioned(
top: _offset.dy,
left: _offset.dx,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
scale: _scale,
title: 'Main',
laneBuilder: widget.laneBuilder,
size: size,
crossAxisCount: widget.crossAxisCount,
children: [
if (initialRoute != null)
_addChild(
RouteSettings(name: initialRoute),
label: 'Initial Route',
),
if (home != null)
_addChild(
null,
label: 'Home',
child: home,
),
],
),
if (routes != null)
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
title: 'Routes',
scale: _scale,
laneBuilder: widget.laneBuilder,
size: size,
crossAxisCount: widget.crossAxisCount,
children: [
for (var i = 0; i < routes!.keys.length; i++)
_addChild(
RouteSettings(name: routes!.keys.toList()[i]),
label: routes!.keys.toList()[i],
),
],
),
if (widget.children != null)
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
laneBuilder: widget.laneBuilder,
title: widget.childrenLabel ?? '',
scale: _scale,
size: size,
children: widget.children!
.map((e) => _addChild(
null,
label: null,
child: e,
customWidget: true,
))
.toList(),
crossAxisCount: widget.crossAxisCount,
),
if (widget.customRoutes != null)
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
laneBuilder: widget.laneBuilder,
title: 'Custom Routes',
scale: _scale,
size: size,
crossAxisCount: widget.crossAxisCount,
children: [
for (var i = 0; i < widget.customRoutes!.length; i++)
_addChild(
widget.customRoutes![i],
label: widget.customRoutes![i].name,
),
],
),
if (widget.customLanes != null)
for (final lane in widget.customLanes!)
_Lane(
cupertinoDevice: widget.cupertinoDevice,
androidDevice: widget.androidDevice,
orientation: widget.orientation,
laneBuilder: widget.laneBuilder,
title: lane.title,
scale: _scale,
size: size,
crossAxisCount: widget.crossAxisCount,
children: lane.children
.map((e) => _addChild(
null,
label: e.label,
child: e.child,
customSize: e.size,
customWidget: true,
))
.toList(),
),
],
),
),
],
],
);
}
PreferredSizeWidget? _buildAppBar([bool compact = false]) {
const kRestartKey = 0;
const kResetKey = 1;
const kZoomInKey = 2;
const kZoomOutKey = 4;
if (widget.hideAppBar) {
return null;
} else {
return widget.customAppBar ??
AppBar(
title: const Text(_kTitle),
actions: compact
? [
PopupMenuButton<int>(
icon: const Icon(Icons.more_vert),
itemBuilder: (context) => [
const PopupMenuItem(
value: kRestartKey,
child: Text('Restart App'),
),
],
onSelected: (val) {
if (val == kRestartKey) {
restart();
}
},
),
]
: [
IconButton(
tooltip: 'Restart App',
icon: const Icon(Icons.refresh),
onPressed: restart,
),
],
);
}
}
}