fl_nodes 0.4.0+1
fl_nodes: ^0.4.0+1 copied to clipboard
A lightweight, scalable, and highly customizable package that empowers Flutter developers to create dynamic, interactive, and visually appealing node-based UIs.
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:example/data_handlers.dart';
import 'package:example/nodes.dart';
import 'package:example/utils/snackbar.dart';
import 'package:example/widgets/instructions.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:http/http.dart' as http;
import 'package:fl_nodes/fl_nodes.dart';
import './widgets/hierarchy.dart';
import './widgets/search.dart';
import 'l10n/app_localizations.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const NodeEditorExampleApp());
}
class NodeEditorExampleApp extends StatefulWidget {
const NodeEditorExampleApp({super.key});
@override
State<NodeEditorExampleApp> createState() => _NodeEditorExampleAppState();
}
class _NodeEditorExampleAppState extends State<NodeEditorExampleApp> {
late Locale _locale;
final locales = [
'en',
'it',
'fr',
'es',
'de',
'ja',
'zh',
'ko',
'ru',
'ar',
];
void _cycleLocale() {
setState(() {
final currentIndex = locales.indexOf(_locale.languageCode);
final nextIndex = (currentIndex + 1) % locales.length;
_locale = Locale(locales[nextIndex]);
});
}
@override
void initState() {
super.initState();
final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
final supportedLanguageCodes = locales.toSet();
final defaultLanguageCode =
supportedLanguageCodes.contains(systemLocale.languageCode)
? systemLocale.languageCode
: 'en';
_locale = Locale(defaultLanguageCode);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate,
const FlNodeEditorLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
...locales.map((lang) => Locale(lang)),
],
locale: _locale,
title: 'Fl Nodes Example',
theme: ThemeData.dark(),
home: NodeEditorExampleScreen(onLocaleToggle: _cycleLocale),
debugShowCheckedModeBanner: kDebugMode,
);
}
}
class NodeEditorExampleScreen extends StatefulWidget {
const NodeEditorExampleScreen({
super.key,
required this.onLocaleToggle,
});
final VoidCallback onLocaleToggle;
@override
State<NodeEditorExampleScreen> createState() =>
NodeEditorExampleScreenState();
}
class NodeEditorExampleScreenState extends State<NodeEditorExampleScreen> {
late final FlNodeEditorController _nodeEditorController;
bool isHierarchyCollapsed = true;
@override
void initState() {
super.initState();
_nodeEditorController = FlNodeEditorController(
projectSaver: (jsonData) async {
final String? outputPath = await FilePicker.platform.saveFile(
dialogTitle: AppLocalizations.of(context)!.saveProjectDialogTitle,
fileName: 'node_project.json',
type: FileType.custom,
allowedExtensions: ['json'],
bytes: utf8.encode(jsonEncode(jsonData)),
);
if (outputPath != null || kIsWeb) {
return true;
} else {
return false;
}
},
projectLoader: (isSaved) async {
if (!isSaved) {
final bool? proceed = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.unsavedChangesTitle),
content: Text(
AppLocalizations.of(context)!.unsavedChangesMsg,
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(AppLocalizations.of(context)!.proceed),
),
],
);
},
);
if (proceed != true) return null;
}
final FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
);
if (result == null) return null;
late final String fileContent;
if (kIsWeb) {
final byteData = result.files.single.bytes!;
fileContent = utf8.decode(byteData.buffer.asUint8List());
} else {
final File file = File(result.files.single.path!);
fileContent = await file.readAsString();
}
return jsonDecode(fileContent);
},
projectCreator: (isSaved) async {
if (isSaved) return true;
final bool? proceed = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.unsavedChangesTitle),
content: Text(
AppLocalizations.of(context)!.unsavedChangesMsg,
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context)!.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(AppLocalizations.of(context)!.proceed),
),
],
);
},
);
return proceed == true;
},
onCallback: (type, message) =>
showNodeEditorSnackbar(context, message, type),
);
registerDataHandlers(_nodeEditorController);
registerNodes(context, _nodeEditorController);
_loadSampleProject();
}
Future<void> _loadSampleProject() async {
const sampleProjectLink =
'https://raw.githubusercontent.com/WilliamKarolDiCioccio/fl_nodes/refs/heads/main/example/assets/www/node_project.json';
final response = await http.get(Uri.parse(sampleProjectLink));
if (response.statusCode == 200 && mounted) {
_nodeEditorController.project.load(
data: jsonDecode(response.body),
context: context,
);
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.failedToLoadSampleProject,
),
backgroundColor: Colors.red,
),
);
}
}
@override
void dispose() {
_nodeEditorController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
HierarchyWidget(
controller: _nodeEditorController,
isCollapsed: isHierarchyCollapsed,
),
Expanded(
child: FlNodeEditorWidget(
controller: _nodeEditorController,
expandToParent: true,
overlay: () {
return [
FlOverlayData(
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
spacing: 8,
children: [
// Hierarchy toggle button
IconButton.filled(
tooltip: AppLocalizations.of(context)!
.toggleHierarchyTooltip,
style: IconButton.styleFrom(
backgroundColor: Colors.blue,
),
onPressed: () => setState(() {
isHierarchyCollapsed = !isHierarchyCollapsed;
}),
icon: Icon(
isHierarchyCollapsed
? Icons.keyboard_arrow_right
: Icons.keyboard_arrow_left,
size: 32,
color: Colors.white,
),
),
// Search widget
SearchWidget(controller: _nodeEditorController),
const Spacer(),
// Locale toggle button
IconButton.filled(
tooltip: AppLocalizations.of(context)!
.cycleLocaleTooltip,
style: IconButton.styleFrom(
backgroundColor: Colors.blue,
),
onPressed: widget.onLocaleToggle,
icon: const Icon(
Icons.translate,
size: 32,
color: Colors.white,
),
),
// Snap to grid toggle button
IconButton.filled(
tooltip: AppLocalizations.of(context)!
.toggleSnapToGridTooltip,
style: IconButton.styleFrom(
backgroundColor: Colors.blue,
),
onPressed: () =>
_nodeEditorController.enableSnapToGrid(
!_nodeEditorController.config.enableSnapToGrid,
),
icon: Icon(
_nodeEditorController.config.enableSnapToGrid
? Icons.grid_on
: Icons.grid_off,
size: 32,
color: Colors.white,
),
),
// Execute graph button
IconButton.filled(
tooltip: AppLocalizations.of(context)!
.executeGraphTooltip,
style: IconButton.styleFrom(
backgroundColor: Colors.blue,
),
onPressed: () => _nodeEditorController.runner
.executeGraph(context: context),
icon: const Icon(
Icons.play_arrow,
size: 32,
color: Colors.white,
),
),
],
),
),
),
FlOverlayData(
bottom: 0,
left: 0,
child: const InstructionsWidget(),
),
];
},
),
),
],
),
),
);
}
}