diff --git a/docs/doc.md b/docs/doc.md index 2b35aa4..99195c4 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -53,6 +53,7 @@ class SduiServiceImpl extends SduiServiceBase { } } } + ``` ## Creating Screens @@ -121,6 +122,8 @@ SduiWidgetData() Display text with styling: ```dart SduiWidgetData() + + ..type = WidgetType.TEXT ..stringAttributes['text'] = 'Hello World' ..textStyle = (TextStyleData() diff --git a/example/lib/json_example.dart b/example/lib/json_example.dart new file mode 100644 index 0000000..8b9365b --- /dev/null +++ b/example/lib/json_example.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sdui/src/parser/sdui_json_parser.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class JsonExample extends StatelessWidget { + const JsonExample({super.key}); + + @override + Widget build(BuildContext context) { + // Example JSON data for a simple profile card + final jsonData = { + 'type': 'CONTAINER', + 'margin': { + 'left': 16.0, + 'top': 16.0, + 'right': 16.0, + 'bottom': 16.0 + }, + 'decoration': { + 'color': '#FFFFFF', + 'borderRadius': { + 'radius': 12.0 + }, + 'boxShadow': [ + { + 'color': '#000000', + 'offsetX': 0.0, + 'offsetY': 2.0, + 'blurRadius': 4.0, + 'spreadRadius': 0.0 + } + ] + }, + 'child': { + 'type': 'COLUMN', + 'crossAxisAlignment': 'START', + 'children': [ + { + 'type': 'IMAGE', + 'src': 'https://picsum.photos/200', + 'width': 200.0, + 'height': 200.0, + 'fit': 'COVER', + 'alignment': 'CENTER' + }, + { + 'type': 'CONTAINER', + 'padding': { + 'left': 16.0, + 'top': 16.0, + 'right': 16.0, + 'bottom': 16.0 + }, + 'child': { + 'type': 'COLUMN', + 'crossAxisAlignment': 'START', + 'children': [ + { + 'type': 'TEXT', + 'text': 'John Doe', + 'style': { + 'color': '#000000', + 'fontSize': 24.0, + 'fontWeight': 'BOLD' + } + }, + { + 'type': 'SIZED_BOX', + 'height': 8.0 + }, + { + 'type': 'TEXT', + 'text': 'Software Developer', + 'style': { + 'color': '#666666', + 'fontSize': 16.0 + } + }, + { + 'type': 'SIZED_BOX', + 'height': 16.0 + }, + { + 'type': 'ROW', + 'mainAxisAlignment': 'SPACE_BETWEEN', + 'children': [ + { + 'type': 'ICON', + 'icon': { + 'codePoint': 0xe0b0, + 'fontFamily': 'MaterialIcons' + }, + 'color': '#2196F3', + 'size': 24.0 + }, + { + 'type': 'ICON', + 'icon': { + 'codePoint': 0xe0c8, + 'fontFamily': 'MaterialIcons' + }, + 'color': '#2196F3', + 'size': 24.0 + }, + { + 'type': 'ICON', + 'icon': { + 'codePoint': 0xe0d2, + 'fontFamily': 'MaterialIcons' + }, + 'color': '#2196F3', + 'size': 24.0 + } + ] + } + ] + } + } + ] + } + }; + + // Parse the JSON data into a widget + final widget = SduiJsonParser.parse(jsonData); + + return MaterialApp( + home: Scaffold( + backgroundColor: Colors.grey[200], + body: Center( + child: widget.toFlutterWidget(), + ), + ), + ); + } +} + +// Example of making a JSON request to your backend +Future> fetchSduiJson() async { + final response = await http.get(Uri.parse('your-api-endpoint')); + if (response.statusCode == 200) { + return json.decode(response.body); + } else { + throw Exception('Failed to load SDUI JSON'); + } +} + +class MySduiScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: fetchSduiJson(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } + + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + + if (!snapshot.hasData) { + return const Text('No data available'); + } + + // Parse the JSON data into a widget + final widget = SduiJsonParser.parse(snapshot.data!); + + return Scaffold( + body: widget.toFlutterWidget(), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/flutter_sdui.dart b/lib/flutter_sdui.dart index a7e5442..501eab3 100644 --- a/lib/flutter_sdui.dart +++ b/lib/flutter_sdui.dart @@ -4,9 +4,10 @@ library; // Export the core parser and widget base -export 'src/parser/sdui_proto_parser.dart'; // New proto parser +export 'src/parser/sdui_proto_parser.dart'; export 'src/widgets/sdui_widget.dart'; + // Export individual widget models export 'src/widgets/sdui_column.dart'; export 'src/widgets/sdui_row.dart'; @@ -27,3 +28,9 @@ export 'src/generated/sdui.pb.dart'; export 'src/generated/sdui.pbgrpc.dart'; export 'src/generated/sdui.pbenum.dart'; export 'src/generated/sdui.pbjson.dart'; + +// Export JSON parser and models +export 'src/parser/sdui_json_parser.dart'; +export 'src/parser/sdui_json_model.dart'; + + diff --git a/lib/src/parser/sdui_json_model.dart b/lib/src/parser/sdui_json_model.dart new file mode 100644 index 0000000..c6bebf5 --- /dev/null +++ b/lib/src/parser/sdui_json_model.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; + + +class SduiJsonWidget { + final String type; + final Map attributes; + final List? children; + + SduiJsonWidget({ + required this.type, + required this.attributes, + this.children, + }); + + factory SduiJsonWidget.fromJson(Map json) { + return SduiJsonWidget( + type: json['type']?.toString().toUpperCase() ?? '', + attributes: json['attributes'] as Map? ?? {}, + children: (json['children'] as List?) + ?.map((child) => SduiJsonWidget.fromJson(child as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'type': type, + 'attributes': attributes, + 'children': children?.map((child) => child.toJson()).toList(), + }; + } +} + +class SduiJsonStyle { + final Color? color; + final double? fontSize; + final FontWeight? fontWeight; + final FontStyle? fontStyle; + final double? letterSpacing; + final double? wordSpacing; + final double? height; + final String? fontFamily; + + SduiJsonStyle({ + this.color, + this.fontSize, + this.fontWeight, + this.fontStyle, + this.letterSpacing, + this.wordSpacing, + this.height, + this.fontFamily, + }); + + factory SduiJsonStyle.fromJson(Map json) { + return SduiJsonStyle( + color: json['color'] != null ? Color(int.parse(json['color'].toString().replaceAll('#', '0xFF'))) : null, + fontSize: json['fontSize']?.toDouble(), + fontWeight: json['fontWeight'] != null ? FontWeight.values.firstWhere( + (e) => e.toString() == 'FontWeight.${json['fontWeight'].toString().toUpperCase()}', + orElse: () => FontWeight.normal, + ) : null, + fontStyle: json['fontStyle'] != null ? FontStyle.values.firstWhere( + (e) => e.toString() == 'FontStyle.${json['fontStyle'].toString().toUpperCase()}', + orElse: () => FontStyle.normal, + ) : null, + letterSpacing: json['letterSpacing']?.toDouble(), + wordSpacing: json['wordSpacing']?.toDouble(), + height: json['height']?.toDouble(), + fontFamily: json['fontFamily'], + ); + } + + Map toJson() { + return { + 'color': color?.value.toRadixString(16).padLeft(8, '0'), + 'fontSize': fontSize, + 'fontWeight': fontWeight?.toString().split('.').last, + 'fontStyle': fontStyle?.toString().split('.').last, + 'letterSpacing': letterSpacing, + 'wordSpacing': wordSpacing, + 'height': height, + 'fontFamily': fontFamily, + }; + } +} + + +class SduiJsonDecoration { + final Color? color; + final SduiJsonBorder? border; + final SduiJsonBorderRadius? borderRadius; + final List? boxShadow; + + SduiJsonDecoration({ + this.color, + this.border, + this.borderRadius, + this.boxShadow, + }); + + factory SduiJsonDecoration.fromJson(Map json) { + return SduiJsonDecoration( + color: json['color'] != null ? Color(int.parse(json['color'].toString().replaceAll('#', '0xFF'))) : null, + border: json['border'] != null ? SduiJsonBorder.fromJson(json['border']) : null, + borderRadius: json['borderRadius'] != null ? SduiJsonBorderRadius.fromJson(json['borderRadius']) : null, + boxShadow: (json['boxShadow'] as List?) + ?.map((shadow) => SduiJsonBoxShadow.fromJson(shadow as Map)) + .toList(), + ); + } + + Map toJson() { + return { + 'color': color?.value.toRadixString(16).padLeft(8, '0'), + 'border': border?.toJson(), + 'borderRadius': borderRadius?.toJson(), + 'boxShadow': boxShadow?.map((shadow) => shadow.toJson()).toList(), + }; + } +} + + +class SduiJsonBorder { + final Color? color; + final double? width; + + SduiJsonBorder({ + this.color, + this.width, + }); + + factory SduiJsonBorder.fromJson(Map json) { + return SduiJsonBorder( + color: json['color'] != null ? Color(int.parse(json['color'].toString().replaceAll('#', '0xFF'))) : null, + width: json['width']?.toDouble(), + ); + } + + Map toJson() { + return { + 'color': color?.value.toRadixString(16).padLeft(8, '0'), + 'width': width, + }; + } +} + + +class SduiJsonBorderRadius { + final double? radius; + + SduiJsonBorderRadius({ + this.radius, + }); + + factory SduiJsonBorderRadius.fromJson(Map json) { + return SduiJsonBorderRadius( + radius: json['radius']?.toDouble(), + ); + } + + Map toJson() { + return { + 'radius': radius, + }; + } +} + + +class SduiJsonBoxShadow { + final Color? color; + final double? offsetX; + final double? offsetY; + final double? blurRadius; + final double? spreadRadius; + + SduiJsonBoxShadow({ + this.color, + this.offsetX, + this.offsetY, + this.blurRadius, + this.spreadRadius, + }); + + factory SduiJsonBoxShadow.fromJson(Map json) { + return SduiJsonBoxShadow( + color: json['color'] != null ? Color(int.parse(json['color'].toString().replaceAll('#', '0xFF'))) : null, + offsetX: json['offsetX']?.toDouble(), + offsetY: json['offsetY']?.toDouble(), + blurRadius: json['blurRadius']?.toDouble(), + spreadRadius: json['spreadRadius']?.toDouble(), + ); + } + + Map toJson() { + return { + 'color': color?.value.toRadixString(16).padLeft(8, '0'), + 'offsetX': offsetX, + 'offsetY': offsetY, + 'blurRadius': blurRadius, + 'spreadRadius': spreadRadius, + }; + } +} \ No newline at end of file diff --git a/lib/src/parser/sdui_json_parser.dart b/lib/src/parser/sdui_json_parser.dart new file mode 100644 index 0000000..eec47db --- /dev/null +++ b/lib/src/parser/sdui_json_parser.dart @@ -0,0 +1,429 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_sdui/src/widgets/sdui_column.dart'; +import 'package:flutter_sdui/src/widgets/sdui_container.dart'; +import 'package:flutter_sdui/src/widgets/sdui_icon.dart'; +import 'package:flutter_sdui/src/widgets/sdui_image.dart'; +import 'package:flutter_sdui/src/widgets/sdui_row.dart'; +import 'package:flutter_sdui/src/widgets/sdui_scaffold.dart'; +import 'package:flutter_sdui/src/widgets/sdui_sized_box.dart'; +import 'package:flutter_sdui/src/widgets/sdui_spacer.dart'; +import 'package:flutter_sdui/src/widgets/sdui_text.dart'; +import 'package:flutter_sdui/src/widgets/sdui_widget.dart'; +import 'package:flutter_sdui/src/parser/sdui_json_model.dart'; + +class SduiJsonParser { + static SduiWidget parse(Map data) { + final String type = data['type']?.toString().toUpperCase() ?? ''; + + switch (type) { + case 'COLUMN': + return _parseColumn(data); + case 'ROW': + return _parseRow(data); + case 'TEXT': + return _parseText(data); + case 'IMAGE': + return _parseImage(data); + case 'SIZED_BOX': + return _parseSizedBox(data); + case 'CONTAINER': + return _parseContainer(data); + case 'SCAFFOLD': + return _parseScaffold(data); + case 'SPACER': + return _parseSpacer(data); + case 'ICON': + return _parseIcon(data); + default: + log('Unsupported widget type: $type'); + return SduiContainer(); + } + } + + static SduiColumn _parseColumn(Map data) { + List children = (data['children'] as List?) + ?.map((child) => parse(child as Map)) + .toList() ?? + []; + + return SduiColumn( + children: children, + mainAxisAlignment: _parseMainAxisAlignment(data['mainAxisAlignment']), + crossAxisAlignment: _parseCrossAxisAlignment(data['crossAxisAlignment']), + mainAxisSize: _parseMainAxisSize(data['mainAxisSize']), + textDirection: _parseTextDirection(data['textDirection']), + verticalDirection: _parseVerticalDirection(data['verticalDirection']), + textBaseline: _parseTextBaseline(data['textBaseline']), + ); + } + + static SduiRow _parseRow(Map data) { + List children = (data['children'] as List?) + ?.map((child) => parse(child as Map)) + .toList() ?? + []; + + return SduiRow( + children: children, + mainAxisAlignment: _parseMainAxisAlignment(data['mainAxisAlignment']), + crossAxisAlignment: _parseCrossAxisAlignment(data['crossAxisAlignment']), + mainAxisSize: _parseMainAxisSize(data['mainAxisSize']), + textDirection: _parseTextDirection(data['textDirection']), + verticalDirection: _parseVerticalDirection(data['verticalDirection']), + textBaseline: _parseTextBaseline(data['textBaseline']), + ); + } + + static SduiText _parseText(Map data) { + String text = data['text']?.toString() ?? ''; + TextStyle? style = data['style'] != null ? _parseTextStyle(data['style']) : null; + + return SduiText( + text, + style: style, + textAlign: _parseTextAlign(data['textAlign']), + overflow: _parseTextOverflow(data['overflow']), + maxLines: data['maxLines'], + softWrap: data['softWrap'], + letterSpacing: data['letterSpacing']?.toDouble(), + wordSpacing: data['wordSpacing']?.toDouble(), + height: data['height']?.toDouble(), + fontFamily: data['fontFamily'], + textDirection: _parseTextDirection(data['textDirection']), + ); + } + + static SduiImage _parseImage(Map data) { + String src = data['src']?.toString() ?? ''; + + return SduiImage( + src, + width: data['width']?.toDouble(), + height: data['height']?.toDouble(), + fit: _parseBoxFit(data['fit']), + alignment: _parseAlignment(data['alignment']), + repeat: _parseImageRepeat(data['repeat']), + color: _parseColor(data['color']), + colorBlendMode: _parseBlendMode(data['colorBlendMode']), + centerSlice: _parseRect(data['centerSlice']), + matchTextDirection: data['matchTextDirection'], + gaplessPlayback: data['gaplessPlayback'], + filterQuality: _parseFilterQuality(data['filterQuality']), + cacheWidth: data['cacheWidth'], + cacheHeight: data['cacheHeight'], + scale: data['scale']?.toDouble(), + semanticLabel: data['semanticLabel'], + errorWidget: data['errorWidget'] != null + ? parse(data['errorWidget']).toFlutterWidget() + : null, + loadingWidget: data['loadingWidget'] != null + ? parse(data['loadingWidget']).toFlutterWidget() + : null, + ); + } + + static SduiSizedBox _parseSizedBox(Map data) { + return SduiSizedBox( + width: data['width']?.toDouble(), + height: data['height']?.toDouble(), + child: data['child'] != null ? parse(data['child']) : null, + ); + } + + static SduiContainer _parseContainer(Map data) { + return SduiContainer( + child: data['child'] != null ? parse(data['child']) : null, + padding: _parseEdgeInsets(data['padding']), + margin: _parseEdgeInsets(data['margin']), + decoration: _parseBoxDecoration(data['decoration']), + width: data['width']?.toDouble(), + height: data['height']?.toDouble(), + color: _parseColor(data['color']), + alignment: _parseAlignment(data['alignment']), + constraints: _parseBoxConstraints(data['constraints']), + transform: _parseTransform(data['transform']), + transformAlignment: _parseAlignmentGeometry(data['transformAlignment']), + ); + } + + static SduiScaffold _parseScaffold(Map data) { + return SduiScaffold( + appBar: data['appBar'] != null ? parse(data['appBar']) : null, + body: data['body'] != null ? parse(data['body']) : null, + floatingActionButton: data['floatingActionButton'] != null + ? parse(data['floatingActionButton']) + : null, + bottomNavigationBar: data['bottomNavigationBar'] != null + ? parse(data['bottomNavigationBar']) + : null, + backgroundColor: _parseColor(data['backgroundColor']), + resizeToAvoidBottomInset: data['resizeToAvoidBottomInset'], + ); + } + + static SduiSpacer _parseSpacer(Map data) { + return SduiSpacer( + flex: data['flex'], + ); + } + + static SduiIcon _parseIcon(Map data) { + return SduiIcon( + icon: _parseIconData(data['icon']), + size: data['size']?.toDouble(), + color: _parseColor(data['color']), + semanticLabel: data['semanticLabel'], + textDirection: _parseTextDirection(data['textDirection']), + ); + } + + // Helper methods for parsing various Flutter types + static MainAxisAlignment _parseMainAxisAlignment(dynamic value) { + if (value == null) return MainAxisAlignment.start; + return MainAxisAlignment.values.firstWhere( + (e) => e.toString() == 'MainAxisAlignment.${value.toString().toUpperCase()}', + orElse: () => MainAxisAlignment.start, + ); + } + + static CrossAxisAlignment _parseCrossAxisAlignment(dynamic value) { + if (value == null) return CrossAxisAlignment.center; + return CrossAxisAlignment.values.firstWhere( + (e) => e.toString() == 'CrossAxisAlignment.${value.toString().toUpperCase()}', + orElse: () => CrossAxisAlignment.center, + ); + } + + static MainAxisSize _parseMainAxisSize(dynamic value) { + if (value == null) return MainAxisSize.max; + return MainAxisSize.values.firstWhere( + (e) => e.toString() == 'MainAxisSize.${value.toString().toUpperCase()}', + orElse: () => MainAxisSize.max, + ); + } + + static TextDirection _parseTextDirection(dynamic value) { + if (value == null) return TextDirection.ltr; + return TextDirection.values.firstWhere( + (e) => e.toString() == 'TextDirection.${value.toString().toUpperCase()}', + orElse: () => TextDirection.ltr, + ); + } + + static VerticalDirection _parseVerticalDirection(dynamic value) { + if (value == null) return VerticalDirection.down; + return VerticalDirection.values.firstWhere( + (e) => e.toString() == 'VerticalDirection.${value.toString().toUpperCase()}', + orElse: () => VerticalDirection.down, + ); + } + + static TextBaseline _parseTextBaseline(dynamic value) { + if (value == null) return TextBaseline.alphabetic; + return TextBaseline.values.firstWhere( + (e) => e.toString() == 'TextBaseline.${value.toString().toUpperCase()}', + orElse: () => TextBaseline.alphabetic, + ); + } + + static TextStyle _parseTextStyle(Map? data) { + if (data == null) return const TextStyle(); + return TextStyle( + color: _parseColor(data['color']), + fontSize: data['fontSize']?.toDouble(), + fontWeight: _parseFontWeight(data['fontWeight']), + fontStyle: _parseFontStyle(data['fontStyle']), + letterSpacing: data['letterSpacing']?.toDouble(), + wordSpacing: data['wordSpacing']?.toDouble(), + height: data['height']?.toDouble(), + fontFamily: data['fontFamily'], + ); + } + + static TextAlign _parseTextAlign(dynamic value) { + if (value == null) return TextAlign.start; + return TextAlign.values.firstWhere( + (e) => e.toString() == 'TextAlign.${value.toString().toUpperCase()}', + orElse: () => TextAlign.start, + ); + } + + static TextOverflow _parseTextOverflow(dynamic value) { + if (value == null) return TextOverflow.clip; + return TextOverflow.values.firstWhere( + (e) => e.toString() == 'TextOverflow.${value.toString().toUpperCase()}', + orElse: () => TextOverflow.clip, + ); + } + + static BoxFit _parseBoxFit(dynamic value) { + if (value == null) return BoxFit.cover; + return BoxFit.values.firstWhere( + (e) => e.toString() == 'BoxFit.${value.toString().toUpperCase()}', + orElse: () => BoxFit.cover, + ); + } + + static Alignment _parseAlignment(dynamic value) { + if (value == null) return Alignment.center; + switch (value.toString().toUpperCase()) { + case 'TOP_LEFT': + return Alignment.topLeft; + case 'TOP_CENTER': + return Alignment.topCenter; + case 'TOP_RIGHT': + return Alignment.topRight; + case 'CENTER_LEFT': + return Alignment.centerLeft; + case 'CENTER': + return Alignment.center; + case 'CENTER_RIGHT': + return Alignment.centerRight; + case 'BOTTOM_LEFT': + return Alignment.bottomLeft; + case 'BOTTOM_CENTER': + return Alignment.bottomCenter; + case 'BOTTOM_RIGHT': + return Alignment.bottomRight; + default: + return Alignment.center; + } + } + + static ImageRepeat _parseImageRepeat(dynamic value) { + if (value == null) return ImageRepeat.noRepeat; + return ImageRepeat.values.firstWhere( + (e) => e.toString() == 'ImageRepeat.${value.toString().toUpperCase()}', + orElse: () => ImageRepeat.noRepeat, + ); + } + + static Color _parseColor(dynamic value) { + if (value == null) return Colors.transparent; + if (value is String) { + return Color(int.parse(value.replaceAll('#', '0xFF'))); + } + return Colors.transparent; + } + + static BlendMode _parseBlendMode(dynamic value) { + if (value == null) return BlendMode.srcOver; + return BlendMode.values.firstWhere( + (e) => e.toString() == 'BlendMode.${value.toString().toUpperCase()}', + orElse: () => BlendMode.srcOver, + ); + } + + static Rect _parseRect(Map? data) { + if (data == null) return Rect.zero; + return Rect.fromLTWH( + data['left']?.toDouble() ?? 0, + data['top']?.toDouble() ?? 0, + data['width']?.toDouble() ?? 0, + data['height']?.toDouble() ?? 0, + ); + } + + static FilterQuality _parseFilterQuality(dynamic value) { + if (value == null) return FilterQuality.low; + return FilterQuality.values.firstWhere( + (e) => e.toString() == 'FilterQuality.${value.toString().toUpperCase()}', + orElse: () => FilterQuality.low, + ); + } + + static EdgeInsets _parseEdgeInsets(Map? data) { + if (data == null) return EdgeInsets.zero; + return EdgeInsets.fromLTRB( + data['left']?.toDouble() ?? 0, + data['top']?.toDouble() ?? 0, + data['right']?.toDouble() ?? 0, + data['bottom']?.toDouble() ?? 0, + ); + } + + static BoxDecoration _parseBoxDecoration(Map? data) { + if (data == null) return const BoxDecoration(); + return BoxDecoration( + color: _parseColor(data['color']), + border: _parseBorder(data['border']), + borderRadius: _parseBorderRadius(data['borderRadius']), + boxShadow: _parseBoxShadow(data['boxShadow']), + ); + } + + static Border _parseBorder(Map? data) { + if (data == null) return Border.all(); + return Border.all( + color: _parseColor(data['color']), + width: data['width']?.toDouble() ?? 1.0, + ); + } + + static BorderRadius _parseBorderRadius(Map? data) { + if (data == null) return BorderRadius.zero; + return BorderRadius.circular(data['radius']?.toDouble() ?? 0); + } + + static List _parseBoxShadow(List? data) { + if (data == null) return []; + return data.map((shadow) { + return BoxShadow( + color: _parseColor(shadow['color']), + offset: Offset( + shadow['offsetX']?.toDouble() ?? 0, + shadow['offsetY']?.toDouble() ?? 0, + ), + blurRadius: shadow['blurRadius']?.toDouble() ?? 0, + spreadRadius: shadow['spreadRadius']?.toDouble() ?? 0, + ); + }).toList(); + } + + static BoxConstraints _parseBoxConstraints(Map? data) { + if (data == null) return const BoxConstraints(); + return BoxConstraints( + minWidth: data['minWidth']?.toDouble(), + maxWidth: data['maxWidth']?.toDouble(), + minHeight: data['minHeight']?.toDouble(), + maxHeight: data['maxHeight']?.toDouble(), + ); + } + + static Matrix4 _parseTransform(List? data) { + if (data == null) return Matrix4.identity(); + return Matrix4.fromList(data.map((e) => (e as num).toDouble()).toList()); + } + + static AlignmentGeometry _parseAlignmentGeometry(dynamic value) { + if (value == null) return Alignment.center; + return _parseAlignment(value); + } + + static IconData _parseIconData(Map? data) { + if (data == null) return Icons.error; + return IconData( + data['codePoint'] ?? 0, + fontFamily: data['fontFamily'], + fontPackage: data['fontPackage'], + ); + } + + static FontWeight _parseFontWeight(dynamic value) { + if (value == null) return FontWeight.normal; + return FontWeight.values.firstWhere( + (e) => e.toString() == 'FontWeight.${value.toString().toUpperCase()}', + orElse: () => FontWeight.normal, + ); + } + + static FontStyle _parseFontStyle(dynamic value) { + if (value == null) return FontStyle.normal; + return FontStyle.values.firstWhere( + (e) => e.toString() == 'FontStyle.${value.toString().toUpperCase()}', + orElse: () => FontStyle.normal, + ); + } +} \ No newline at end of file diff --git a/lib/src/parser/sdui_proto_parser.dart b/lib/src/parser/sdui_proto_parser.dart index 9411281..16de376 100644 --- a/lib/src/parser/sdui_proto_parser.dart +++ b/lib/src/parser/sdui_proto_parser.dart @@ -18,11 +18,11 @@ import 'package:flutter_sdui/src/widgets/sdui_widget.dart'; class SduiParser { // Parse method for JSON data static SduiWidget parseJSON(Map data) { - // TODO: Implement JSON parsing logic + throw UnimplementedError('JSON parser not fully implemented'); } - // Parse from Protobuf data model + static SduiWidget parseProto(SduiWidgetData data) { switch (data.type) { case WidgetType.COLUMN: @@ -49,7 +49,7 @@ class SduiParser { } } - // Helper methods to parse specific widget types from protobuf + static SduiColumn _parseProtoColumn(SduiWidgetData data) { List children = data.children.map((child) => SduiParser.parseProto(child)).toList(); @@ -87,7 +87,7 @@ class SduiParser { TextStyle? style = data.hasTextStyle() ? _parseProtoTextStyle(data.textStyle) : null; - // Parse additional text properties + TextAlign? textAlign = _parseProtoTextAlign(data.textAlign); TextOverflow? overflow = _parseProtoTextOverflow(data.overflow); int? maxLines = data.hasMaxLines() ? data.maxLines : null; @@ -119,7 +119,7 @@ class SduiParser { double? height = data.doubleAttributes['height']; BoxFit? fit = _parseProtoBoxFit(data.stringAttributes['fit']); - // Parse additional image properties + Alignment? alignment = _parseProtoAlignment(data.alignment); ImageRepeat? repeat = _parseProtoImageRepeat(data.repeat); Color? color = data.hasColor() ? _parseProtoColor(data.color) : null; @@ -136,7 +136,7 @@ class SduiParser { double? scale = data.hasScale() ? data.scale : null; String? semanticLabel = data.hasSemanticLabel() ? data.semanticLabel : null; - // Parse error and loading widgets + Widget? errorWidget = data.hasErrorWidget() ? SduiParser.parseProto(data.errorWidget).toFlutterWidget() : null; @@ -188,7 +188,7 @@ class SduiParser { double? height = data.doubleAttributes['height']; Color? color = data.hasColor() ? _parseProtoColor(data.color) : null; - // Parse additional container properties + Alignment? alignment = _parseProtoAlignment(data.alignment); BoxConstraints? constraints = data.hasConstraints() ? _parseProtoBoxConstraints(data.constraints) @@ -227,7 +227,7 @@ class SduiParser { ? _parseProtoColor(data.backgroundColor) : null; - // Parse additional scaffold properties + SduiWidget? bottomNavigationBar = data.hasBottomNavigationBar() ? SduiParser.parseProto(data.bottomNavigationBar) : null; @@ -293,7 +293,7 @@ class SduiParser { Color? color = data.icon.hasColor() ? _parseProtoColor(data.icon.color) : null; - // Parse additional icon properties + String? semanticLabel = data.hasSemanticLabel() ? data.semanticLabel : null; TextDirection? textDirection = _parseProtoTextDirection(data.textDirection); double? opacity = data.hasOpacity() ? data.opacity : null; @@ -315,7 +315,7 @@ class SduiParser { ); } - // Helper methods for parsing protobuf attribute types + static BoxFit? _parseProtoBoxFit(String? value) { if (value == null) return null; @@ -417,7 +417,7 @@ class SduiParser { static IconData? _parseProtoIconData(IconDataMessage data) { if (data.hasName()) { - // Map common icon names to Material icons (expand as needed) + switch (data.name.toLowerCase()) { case 'settings': return Icons.settings; @@ -434,7 +434,7 @@ class SduiParser { } } - // Fallback to codePoint if available + if (data.hasCodePoint()) { return IconData( data.codePoint, @@ -451,7 +451,7 @@ class SduiParser { borderRadius: data.hasBorderRadius() ? _parseProtoBorderRadius(data.borderRadius) : null, - // Add more properties as needed + ); } @@ -486,10 +486,10 @@ class SduiParser { ); } - return BorderRadius.circular(8.0); // Example default value + return BorderRadius.circular(8.0); } - // New helper methods for parsing new property types + static MainAxisAlignment? _parseProtoMainAxisAlignment( MainAxisAlignmentProto proto) { @@ -606,7 +606,7 @@ class SduiParser { } static AlignmentGeometry? _parseProtoAlignmentGeometry(AlignmentData? data) { - // For now, just use the same alignment parsing + return _parseProtoAlignment(data); } diff --git a/test/parser/sdui_json_parser_test.dart b/test/parser/sdui_json_parser_test.dart new file mode 100644 index 0000000..5de6f08 --- /dev/null +++ b/test/parser/sdui_json_parser_test.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_sdui/src/parser/sdui_json_parser.dart'; +import 'package:flutter_sdui/src/widgets/sdui_column.dart'; +import 'package:flutter_sdui/src/widgets/sdui_row.dart'; +import 'package:flutter_sdui/src/widgets/sdui_text.dart'; +import 'package:flutter_sdui/src/widgets/sdui_container.dart'; +import 'package:flutter_sdui/src/widgets/sdui_image.dart'; +import 'package:flutter_sdui/src/widgets/sdui_scaffold.dart'; + +void main() { + group('SduiJsonParser Tests', () { + test('Parse Column Widget', () { + final jsonData = { + 'type': 'COLUMN', + 'mainAxisAlignment': 'CENTER', + 'crossAxisAlignment': 'START', + 'children': [ + { + 'type': 'TEXT', + 'text': 'Hello', + 'style': { + 'color': '#FF0000', + 'fontSize': 20.0, + } + }, + { + 'type': 'TEXT', + 'text': 'World', + 'style': { + 'color': '#0000FF', + 'fontSize': 16.0, + } + } + ] + }; + + final widget = SduiJsonParser.parse(jsonData); + expect(widget, isA()); + + final column = widget as SduiColumn; + expect(column.mainAxisAlignment, MainAxisAlignment.center); + expect(column.crossAxisAlignment, CrossAxisAlignment.start); + expect(column.children.length, 2); + + expect(column.children[0], isA()); + expect(column.children[1], isA()); + + final text1 = column.children[0] as SduiText; + final text2 = column.children[1] as SduiText; + + expect(text1.text, 'Hello'); + expect(text1.style?.color, const Color(0xFFFF0000)); + expect(text1.style?.fontSize, 20.0); + + expect(text2.text, 'World'); + expect(text2.style?.color, const Color(0xFF0000FF)); + expect(text2.style?.fontSize, 16.0); + }); + + test('Parse Row Widget', () { + final jsonData = { + 'type': 'ROW', + 'mainAxisAlignment': 'SPACE_BETWEEN', + 'children': [ + { + 'type': 'TEXT', + 'text': 'Left', + }, + { + 'type': 'TEXT', + 'text': 'Right', + } + ] + }; + + final widget = SduiJsonParser.parse(jsonData); + expect(widget, isA()); + + final row = widget as SduiRow; + expect(row.mainAxisAlignment, MainAxisAlignment.spaceBetween); + expect(row.children.length, 2); + + expect(row.children[0], isA()); + expect(row.children[1], isA()); + + final text1 = row.children[0] as SduiText; + final text2 = row.children[1] as SduiText; + + expect(text1.text, 'Left'); + expect(text2.text, 'Right'); + }); + + test('Parse Container Widget', () { + final jsonData = { + 'type': 'CONTAINER', + 'width': 200.0, + 'height': 100.0, + 'color': '#FF00FF', + 'padding': { + 'left': 10.0, + 'top': 20.0, + 'right': 10.0, + 'bottom': 20.0 + }, + 'decoration': { + 'borderRadius': { + 'radius': 8.0 + }, + 'boxShadow': [ + { + 'color': '#000000', + 'offsetX': 2.0, + 'offsetY': 2.0, + 'blurRadius': 4.0 + } + ] + }, + 'child': { + 'type': 'TEXT', + 'text': 'Container Text' + } + }; + + final widget = SduiJsonParser.parse(jsonData); + expect(widget, isA()); + + final container = widget as SduiContainer; + expect(container.width, 200.0); + expect(container.height, 100.0); + expect(container.color, const Color(0xFFFF00FF)); + + final padding = container.padding as EdgeInsets; + expect(padding.left, 10.0); + expect(padding.top, 20.0); + expect(padding.right, 10.0); + expect(padding.bottom, 20.0); + + final decoration = container.decoration as BoxDecoration; + expect(decoration.borderRadius, BorderRadius.circular(8.0)); + expect(decoration.boxShadow?.length, 1); + + final shadow = decoration.boxShadow![0]; + expect(shadow.color, const Color(0xFF000000)); + expect(shadow.offset.dx, 2.0); + expect(shadow.offset.dy, 2.0); + expect(shadow.blurRadius, 4.0); + + expect(container.child, isA()); + final text = container.child as SduiText; + expect(text.text, 'Container Text'); + }); + + test('Parse Image Widget', () { + final jsonData = { + 'type': 'IMAGE', + 'src': 'https://example.com/image.jpg', + 'width': 300.0, + 'height': 200.0, + 'fit': 'COVER', + 'alignment': 'CENTER', + 'color': '#808080', + 'colorBlendMode': 'MULTIPLY' + }; + + final widget = SduiJsonParser.parse(jsonData); + expect(widget, isA()); + + final image = widget as SduiImage; + expect(image.src, 'https://example.com/image.jpg'); + expect(image.width, 300.0); + expect(image.height, 200.0); + expect(image.fit, BoxFit.cover); + expect(image.alignment, Alignment.center); + expect(image.color, const Color(0xFF808080)); + expect(image.colorBlendMode, BlendMode.multiply); + }); + + test('Parse Scaffold Widget', () { + final jsonData = { + 'type': 'SCAFFOLD', + 'backgroundColor': '#FFFFFF', + 'appBar': { + 'type': 'CONTAINER', + 'height': 56.0, + 'color': '#2196F3', + 'child': { + 'type': 'TEXT', + 'text': 'App Bar', + 'style': { + 'color': '#FFFFFF', + 'fontSize': 20.0 + } + } + }, + 'body': { + 'type': 'COLUMN', + 'children': [ + { + 'type': 'TEXT', + 'text': 'Body Content' + } + ] + } + }; + + final widget = SduiJsonParser.parse(jsonData); + expect(widget, isA()); + + final scaffold = widget as SduiScaffold; + expect(scaffold.backgroundColor, const Color(0xFFFFFFFF)); + + expect(scaffold.appBar, isA()); + final appBar = scaffold.appBar as SduiContainer; + expect(appBar.height, 56.0); + expect(appBar.color, const Color(0xFF2196F3)); + + expect(appBar.child, isA()); + final appBarText = appBar.child as SduiText; + expect(appBarText.text, 'App Bar'); + expect(appBarText.style?.color, const Color(0xFFFFFFFF)); + expect(appBarText.style?.fontSize, 20.0); + + expect(scaffold.body, isA()); + final body = scaffold.body as SduiColumn; + expect(body.children.length, 1); + + expect(body.children[0], isA()); + final bodyText = body.children[0] as SduiText; + expect(bodyText.text, 'Body Content'); + }); + + test('Parse Invalid Widget Type', () { + final jsonData = { + 'type': 'INVALID_TYPE', + 'text': 'This should not be parsed' + }; + + final widget = SduiJsonParser.parse(jsonData); + expect(widget, isA()); + }); + }); +} \ No newline at end of file