Skip to content
Merged
Show file tree
Hide file tree
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
77 changes: 65 additions & 12 deletions packages/flame_devtools/lib/widgets/component_snapshot.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flame/flame.dart';
import 'dart:convert';
import 'dart:ui' as ui;

import 'package:flame/widgets.dart';
import 'package:flame_devtools/repository.dart';
import 'package:flutter/material.dart' hide Image;
Expand Down Expand Up @@ -52,7 +54,13 @@ class _ComponentSnapshotState extends State<ComponentSnapshot> {
}
}

class Base64Image extends StatelessWidget {
/// Displays an image decoded from a base64 data string.
///
/// The decoded [ui.Image] is held by this widget and disposed when the widget
/// is removed or when [base64] / [imageId] changes — without going through
/// the global Flame images cache, so the same component id can show
/// different snapshots over time without serving a stale cached frame.
class Base64Image extends StatefulWidget {
const Base64Image({
required this.base64,
required this.imageId,
Expand All @@ -62,23 +70,68 @@ class Base64Image extends StatelessWidget {
final String base64;
final String imageId;

@override
State<Base64Image> createState() => _Base64ImageState();
}

class _Base64ImageState extends State<Base64Image> {
late Future<ui.Image> _imageFuture;
ui.Image? _image;

@override
void initState() {
super.initState();
_imageFuture = _decode(widget.base64);
}

@override
void didUpdateWidget(Base64Image oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.base64 != widget.base64 ||
oldWidget.imageId != widget.imageId) {
_disposeImage();
_imageFuture = _decode(widget.base64);
}
}

@override
void dispose() {
_disposeImage();
super.dispose();
}

Future<ui.Image> _decode(String base64Data) async {
final commaIndex = base64Data.indexOf(',');
final payload = commaIndex == -1
? base64Data
: base64Data.substring(commaIndex + 1);
final bytes = base64.decode(payload);
final image = await decodeImageFromList(bytes);
if (mounted) {
_image = image;
} else {
image.dispose();
}
return image;
}

void _disposeImage() {
_image?.dispose();
_image = null;
}

@override
Widget build(BuildContext context) {
final imageFuture = Flame.images.fromBase64(
imageId,
base64,
);
return FutureBuilder(
future: imageFuture,
return FutureBuilder<ui.Image>(
future: _imageFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
return SizedBox(
width: 200,
height: 200,
child: SpriteWidget(
sprite: Sprite(
snapshot.data!,
),
sprite: Sprite(snapshot.data!),
),
);
}
Expand Down
72 changes: 34 additions & 38 deletions packages/flame_devtools/lib/widgets/component_tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,45 +59,41 @@ class ComponentTreeSection extends ConsumerWidget {
),
),
Expanded(
child: SingleChildScrollView(
child: TreeView.simple(
showRootNode: false,
shrinkWrap: true,
indentation: const Indentation(
color: Colors.blue,
style: IndentStyle.roundJoint,
),
onTreeReady: (controller) =>
controller.expandAllChildren(controller.tree),
padding: const EdgeInsets.only(left: 20),
expansionIndicatorBuilder: (context, node) => node.isLeaf
? NoExpansionIndicator(tree: node)
: ChevronIndicator.rightDown(
tree: node,
alignment: Alignment.centerLeft,
),
builder: (context, node) {
return Padding(
padding: node.isLeaf
? EdgeInsets.zero
: const EdgeInsets.only(left: 20),
child: ListTile(
key: Key(
node.data?.id.toString() ?? node.key,
),
selected: node == selectedTreeNode,
selectedColor: theme.colorScheme.primary,
title: Text(node.data!.name),
subtitle: Text(node.data!.id.toString()),
onTap: () {
ref.read(selectedTreeNodeProvider.notifier).state =
node;
},
),
);
},
tree: loadedModel.treeRoot,
child: TreeView.simple(
showRootNode: false,
indentation: const Indentation(
color: Colors.blue,
style: IndentStyle.roundJoint,
),
onTreeReady: (controller) =>
controller.expandAllChildren(controller.tree),
padding: const EdgeInsets.only(left: 20),
expansionIndicatorBuilder: (context, node) => node.isLeaf
? NoExpansionIndicator(tree: node)
: ChevronIndicator.rightDown(
tree: node,
alignment: Alignment.centerLeft,
),
builder: (context, node) {
return Padding(
padding: node.isLeaf
? EdgeInsets.zero
: const EdgeInsets.only(left: 20),
child: ListTile(
key: Key(
node.data?.id.toString() ?? node.key,
),
selected: node == selectedTreeNode,
selectedColor: theme.colorScheme.primary,
title: Text(node.data!.name),
subtitle: Text(node.data!.id.toString()),
onTap: () {
ref.read(selectedTreeNodeProvider.notifier).state = node;
},
),
);
},
tree: loadedModel.treeRoot,
),
),
],
Expand Down
Loading