From f560958938b78f2111f05e845470583360760fa6 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 14 Feb 2024 23:38:52 -0600 Subject: [PATCH] Sample menu exploration --- .../dependencies/pub_dependencies_beta.json | 2 +- .../dependencies/pub_dependencies_main.json | 2 +- .../dependencies/pub_dependencies_stable.json | 2 +- pkgs/samples/README.md | 2 + pkgs/samples/lib/brick_breaker.dart | 326 ++++++++ pkgs/samples/lib/google_ai.dart | 339 +++++++++ pkgs/samples/lib/samples.json | 16 + pkgs/samples/pubspec.yaml | 4 + pkgs/samples/tool/samples.dart | 21 +- pkgs/sketch_pad/assets/dart_logo_128.png | Bin 3081 -> 0 bytes pkgs/sketch_pad/assets/dart_logo_192.png | Bin 0 -> 4436 bytes pkgs/sketch_pad/assets/flame_logo_192.png | Bin 0 -> 15606 bytes pkgs/sketch_pad/assets/gemini_sparkle_192.png | Bin 0 -> 5198 bytes pkgs/sketch_pad/lib/main.dart | 25 +- pkgs/sketch_pad/lib/samples.g.dart | 713 +++++++++++++++++- pkgs/sketch_pad/lib/utils.dart | 10 - pkgs/sketch_pad/lib/widgets.dart | 19 + pkgs/sketch_pad/pubspec.yaml | 4 +- 18 files changed, 1444 insertions(+), 41 deletions(-) create mode 100644 pkgs/samples/lib/brick_breaker.dart create mode 100644 pkgs/samples/lib/google_ai.dart delete mode 100644 pkgs/sketch_pad/assets/dart_logo_128.png create mode 100644 pkgs/sketch_pad/assets/dart_logo_192.png create mode 100644 pkgs/sketch_pad/assets/flame_logo_192.png create mode 100644 pkgs/sketch_pad/assets/gemini_sparkle_192.png diff --git a/pkgs/dart_services/tool/dependencies/pub_dependencies_beta.json b/pkgs/dart_services/tool/dependencies/pub_dependencies_beta.json index fe94f8a5c..f72f802f9 100644 --- a/pkgs/dart_services/tool/dependencies/pub_dependencies_beta.json +++ b/pkgs/dart_services/tool/dependencies/pub_dependencies_beta.json @@ -46,7 +46,7 @@ "glob": "2.1.2", "go_router": "13.2.0", "google_fonts": "6.1.0", - "google_generative_ai": "0.1.0", + "google_generative_ai": "0.2.0", "hooks_riverpod": "2.4.10", "html": "0.15.4", "http": "1.2.0", diff --git a/pkgs/dart_services/tool/dependencies/pub_dependencies_main.json b/pkgs/dart_services/tool/dependencies/pub_dependencies_main.json index 8e3b5fd1d..aaefa33bd 100644 --- a/pkgs/dart_services/tool/dependencies/pub_dependencies_main.json +++ b/pkgs/dart_services/tool/dependencies/pub_dependencies_main.json @@ -46,7 +46,7 @@ "glob": "2.1.2", "go_router": "13.2.0", "google_fonts": "6.1.0", - "google_generative_ai": "0.1.0", + "google_generative_ai": "0.2.0", "hooks_riverpod": "2.4.10", "html": "0.15.4", "http": "1.2.0", diff --git a/pkgs/dart_services/tool/dependencies/pub_dependencies_stable.json b/pkgs/dart_services/tool/dependencies/pub_dependencies_stable.json index d2a27ee96..b5ca9b506 100644 --- a/pkgs/dart_services/tool/dependencies/pub_dependencies_stable.json +++ b/pkgs/dart_services/tool/dependencies/pub_dependencies_stable.json @@ -46,7 +46,7 @@ "glob": "2.1.2", "go_router": "13.2.0", "google_fonts": "6.1.0", - "google_generative_ai": "0.1.0", + "google_generative_ai": "0.2.0", "hooks_riverpod": "2.4.10", "html": "0.15.4", "http": "1.2.0", diff --git a/pkgs/samples/README.md b/pkgs/samples/README.md index 12b54562b..bd28f3df7 100644 --- a/pkgs/samples/README.md +++ b/pkgs/samples/README.md @@ -9,6 +9,8 @@ Sample code snippets for DartPad. | --- | --- | --- | --- | | Dart | Fibonacci | [fibonacci.dart](lib/fibonacci.dart) | `fibonacci` | | Dart | Hello world | [hello_world.dart](lib/hello_world.dart) | `hello-world` | +| Ecosystem | Flame game | [brick_breaker.dart](lib/brick_breaker.dart) | `flame-game` | +| Ecosystem | Google AI SDK | [google_ai.dart](lib/google_ai.dart) | `google-ai-sdk` | | Flutter | Counter | [main.dart](lib/main.dart) | `counter` | | Flutter | Sunflower | [sunflower.dart](lib/sunflower.dart) | `sunflower` | diff --git a/pkgs/samples/lib/brick_breaker.dart b/pkgs/samples/lib/brick_breaker.dart new file mode 100644 index 000000000..5437b0c54 --- /dev/null +++ b/pkgs/samples/lib/brick_breaker.dart @@ -0,0 +1,326 @@ +// Copyright 2024 the Dart project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +/// A simplified brick-breaker game, +/// built using the Flame game engine for Flutter. +/// +/// To learn how to build a more complete version of this game yourself, +/// check out the codelab at https://docs.flutter.dev/brick-breaker. +library; + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() { + runApp(const GameApp()); +} + +class GameApp extends StatefulWidget { + const GameApp({super.key}); + + @override + State createState() => _GameAppState(); +} + +class _GameAppState extends State { + late final BrickBreaker game; + + @override + void initState() { + super.initState(); + game = BrickBreaker(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xffa9d6e5), + Color(0xfff2e8cf), + ], + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: FittedBox( + child: SizedBox( + width: gameWidth, + height: gameHeight, + child: GameWidget( + game: game, + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +class BrickBreaker extends FlameGame + with HasCollisionDetection, KeyboardEvents, TapDetector { + BrickBreaker() + : super( + camera: CameraComponent.withFixedResolution( + width: gameWidth, height: gameHeight)); + + final rand = math.Random(); + double get width => size.x; + double get height => size.y; + + @override + FutureOr onLoad() async { + super.onLoad(); + camera.viewfinder.anchor = Anchor.topLeft; + world.add(PlayArea()); + startGame(); + } + + void startGame() { + world.removeAll(world.children.query()); + world.removeAll(world.children.query()); + world.removeAll(world.children.query()); + + world.add(Ball( + difficultyModifier: difficultyModifier, + radius: ballRadius, + position: size / 2, + velocity: + Vector2((rand.nextDouble() - 0.5) * width, height * 0.2).normalized() + ..scale(height / 4), + )); + + world.add(Bat( + size: Vector2(batWidth, batHeight), + cornerRadius: const Radius.circular(ballRadius / 2), + position: Vector2(width / 2, height * 0.95), + )); + + world.addAll([ + for (var i = 0; i < brickColors.length; i++) + for (var j = 1; j <= 5; j++) + Brick( + Vector2( + (i + 0.5) * brickWidth + (i + 1) * brickGutter, + (j + 2.0) * brickHeight + j * brickGutter, + ), + brickColors[i], + ), + ]); + } + + @override + void onTap() { + super.onTap(); + startGame(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, Set keysPressed) { + super.onKeyEvent(event, keysPressed); + switch (event.logicalKey) { + case LogicalKeyboardKey.arrowLeft: + case LogicalKeyboardKey.keyA: + world.children.query().first.moveBy(-batStep); + case LogicalKeyboardKey.keyD: + case LogicalKeyboardKey.arrowRight: + world.children.query().first.moveBy(batStep); + case LogicalKeyboardKey.space: + case LogicalKeyboardKey.enter: + startGame(); + } + return KeyEventResult.handled; + } + + @override + Color backgroundColor() => const Color(0xfff2e8cf); +} + +class Ball extends CircleComponent + with CollisionCallbacks, HasGameReference { + Ball({ + required this.velocity, + required super.position, + required double radius, + required this.difficultyModifier, + }) : super( + radius: radius, + anchor: Anchor.center, + paint: Paint() + ..color = const Color(0xff1e6091) + ..style = PaintingStyle.fill, + children: [CircleHitbox()]); + + final Vector2 velocity; + final double difficultyModifier; + + @override + void update(double dt) { + super.update(dt); + position += velocity * dt; + } + + @override + void onCollisionStart( + Set intersectionPoints, PositionComponent other) { + super.onCollisionStart(intersectionPoints, other); + if (other is PlayArea) { + if (intersectionPoints.first.y <= 0) { + velocity.y = -velocity.y; + } else if (intersectionPoints.first.x <= 0) { + velocity.x = -velocity.x; + } else if (intersectionPoints.first.x >= game.width) { + velocity.x = -velocity.x; + } else if (intersectionPoints.first.y >= game.height) { + add(RemoveEffect( + delay: 0.35, + onComplete: () { + game.startGame(); + }, + )); + } + } else if (other is Bat) { + velocity.y = -velocity.y; + velocity.x = velocity.x + + (position.x - other.position.x) / other.size.x * game.width * 0.3; + } else if (other is Brick) { + if (position.y < other.position.y - other.size.y / 2) { + velocity.y = -velocity.y; + } else if (position.y > other.position.y + other.size.y / 2) { + velocity.y = -velocity.y; + } else if (position.x < other.position.x) { + velocity.x = -velocity.x; + } else if (position.x > other.position.x) { + velocity.x = -velocity.x; + } + velocity.setFrom(velocity * difficultyModifier); + } + } +} + +class Bat extends PositionComponent + with DragCallbacks, HasGameReference { + Bat({ + required this.cornerRadius, + required super.position, + required super.size, + }) : super(anchor: Anchor.center, children: [RectangleHitbox()]); + + final Radius cornerRadius; + + final _paint = Paint() + ..color = const Color(0xff1e6091) + ..style = PaintingStyle.fill; + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas.drawRRect( + RRect.fromRectAndRadius( + Offset.zero & size.toSize(), + cornerRadius, + ), + _paint, + ); + } + + @override + void onDragUpdate(DragUpdateEvent event) { + if (isRemoved) return; + super.onDragUpdate(event); + position.x = (position.x + event.localDelta.x) + .clamp(width / 2, game.width - width / 2); + } + + void moveBy(double dx) { + add(MoveToEffect( + Vector2( + (position.x + dx).clamp(width / 2, game.width - width / 2), + position.y, + ), + EffectController(duration: 0.1), + )); + } +} + +class Brick extends RectangleComponent + with CollisionCallbacks, HasGameReference { + Brick(Vector2 position, Color color) + : super( + position: position, + size: Vector2(brickWidth, brickHeight), + anchor: Anchor.center, + paint: Paint() + ..color = color + ..style = PaintingStyle.fill, + children: [RectangleHitbox()], + ); + + @override + void onCollisionStart( + Set intersectionPoints, PositionComponent other) { + super.onCollisionStart(intersectionPoints, other); + removeFromParent(); + + if (game.world.children.query().length == 1) { + game.startGame(); + } + } +} + +class PlayArea extends RectangleComponent with HasGameReference { + PlayArea() : super(children: [RectangleHitbox()]); + + @override + Future onLoad() async { + super.onLoad(); + size = Vector2(game.width, game.height); + } +} + +const brickColors = [ + Color(0xfff94144), + Color(0xfff3722c), + Color(0xfff8961e), + Color(0xfff9844a), + Color(0xfff9c74f), + Color(0xff90be6d), + Color(0xff43aa8b), + Color(0xff4d908e), + Color(0xff277da1), + Color(0xff577590), +]; + +const gameWidth = 820.0; +const gameHeight = 1600.0; +const ballRadius = gameWidth * 0.02; +const batWidth = gameWidth * 0.2; +const batHeight = ballRadius * 2; +const batStep = gameWidth * 0.05; +const brickGutter = gameWidth * 0.015; +final brickWidth = + (gameWidth - (brickGutter * (brickColors.length + 1))) / brickColors.length; +const brickHeight = gameHeight * 0.03; +const difficultyModifier = 1.05; diff --git a/pkgs/samples/lib/google_ai.dart b/pkgs/samples/lib/google_ai.dart new file mode 100644 index 000000000..ff783d240 --- /dev/null +++ b/pkgs/samples/lib/google_ai.dart @@ -0,0 +1,339 @@ +// Copyright 2024 the Dart project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:google_generative_ai/google_generative_ai.dart'; +import 'package:url_launcher/link.dart'; + +void main() { + runApp(const GenerativeAISample()); +} + +class GenerativeAISample extends StatelessWidget { + const GenerativeAISample({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Flutter + Generative AI', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + brightness: Brightness.dark, + seedColor: const Color.fromARGB(255, 171, 222, 244), + ), + useMaterial3: true, + ), + home: const ChatScreen(title: 'Flutter + Generative AI'), + ); + } +} + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key, required this.title}); + + final String title; + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + String? apiKey; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: switch (apiKey) { + final providedKey? => ChatWidget(apiKey: providedKey), + _ => ApiKeyWidget(onSubmitted: (key) { + setState(() => apiKey = key); + }), + }, + ); + } +} + +class ApiKeyWidget extends StatelessWidget { + ApiKeyWidget({required this.onSubmitted, super.key}); + + final ValueChanged onSubmitted; + final TextEditingController _textController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'To use the Gemini API, you\'ll need an API key. ' + 'If you don\'t already have one, ' + 'create a key in Google AI Studio.', + ), + const SizedBox(height: 8), + Link( + uri: Uri.https('makersuite.google.com', '/app/apikey'), + target: LinkTarget.blank, + builder: (context, followLink) => TextButton( + onPressed: followLink, + child: const Text('Get an API Key'), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + decoration: + textFieldDecoration(context, 'Enter your API key'), + controller: _textController, + onSubmitted: (value) { + onSubmitted(value); + }, + ), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () { + onSubmitted(_textController.value.text); + }, + child: const Text('Submit'), + ), + ], + ), + ], + ), + ), + ); + } +} + +class ChatWidget extends StatefulWidget { + const ChatWidget({required this.apiKey, super.key}); + + final String apiKey; + + @override + State createState() => _ChatWidgetState(); +} + +class _ChatWidgetState extends State { + late final GenerativeModel _model; + late final ChatSession _chat; + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(debugLabel: 'TextField'); + bool _loading = false; + + @override + void initState() { + super.initState(); + _model = GenerativeModel( + model: 'gemini-pro', + apiKey: widget.apiKey, + ); + _chat = _model.startChat(); + } + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration( + milliseconds: 750, + ), + curve: Curves.easeOutCirc, + ), + ); + } + + @override + Widget build(BuildContext context) { + final history = _chat.history.toList(); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + final content = history[idx]; + final text = content.parts + .whereType() + .map((e) => e.text) + .join(''); + return MessageWidget( + text: text, + isFromUser: content.role == 'user', + ); + }, + itemCount: history.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + focusNode: _textFieldFocus, + decoration: + textFieldDecoration(context, 'Enter a prompt...'), + controller: _textController, + onSubmitted: (String value) { + _sendChatMessage(value); + }, + ), + ), + const SizedBox.square(dimension: 15), + if (!_loading) + IconButton( + onPressed: () async { + _sendChatMessage(_textController.text); + }, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.primary, + ), + ) + else + const CircularProgressIndicator(), + ], + ), + ), + ], + ), + ); + } + + Future _sendChatMessage(String message) async { + setState(() { + _loading = true; + }); + + try { + final response = await _chat.sendMessage( + Content.text(message), + ); + final text = response.text; + + if (text == null) { + _showError('Empty response.'); + return; + } else { + setState(() { + _loading = false; + _scrollDown(); + }); + } + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + + void _showError(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Something went wrong'), + content: SingleChildScrollView( + child: Text(message), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ) + ], + ); + }, + ); + } +} + +class MessageWidget extends StatelessWidget { + const MessageWidget({ + super.key, + required this.text, + required this.isFromUser, + }); + + final String text; + final bool isFromUser; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: + isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + constraints: const BoxConstraints(maxWidth: 480), + decoration: BoxDecoration( + color: isFromUser + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(18), + ), + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 20, + ), + margin: const EdgeInsets.only(bottom: 8), + child: MarkdownBody(data: text), + ), + ), + ], + ); + } +} + +InputDecoration textFieldDecoration(BuildContext context, String hintText) => + InputDecoration( + contentPadding: const EdgeInsets.all(15), + hintText: hintText, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(14), + ), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(14), + ), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ); diff --git a/pkgs/samples/lib/samples.json b/pkgs/samples/lib/samples.json index 4547118d2..811cd818e 100644 --- a/pkgs/samples/lib/samples.json +++ b/pkgs/samples/lib/samples.json @@ -6,23 +6,39 @@ "samples": [ { "category": "Dart", + "icon": "dart", "name": "Hello world", "path": "lib/hello_world.dart" }, { "category": "Dart", + "icon": "dart", "name": "Fibonacci", "path": "lib/fibonacci.dart" }, { "category": "Flutter", + "icon": "flutter", "name": "Counter", "path": "lib/main.dart" }, { "category": "Flutter", + "icon": "flutter", "name": "Sunflower", "path": "lib/sunflower.dart" + }, + { + "category": "Ecosystem", + "icon": "gemini", + "name": "Google AI SDK", + "path": "lib/google_ai.dart" + }, + { + "category": "Ecosystem", + "icon": "flame", + "name": "Flame game", + "path": "lib/brick_breaker.dart" } ] } \ No newline at end of file diff --git a/pkgs/samples/pubspec.yaml b/pkgs/samples/pubspec.yaml index 414e9b9bf..a0c4ba60a 100644 --- a/pkgs/samples/pubspec.yaml +++ b/pkgs/samples/pubspec.yaml @@ -6,8 +6,12 @@ environment: sdk: ^3.2.0 dependencies: + flame: ^1.15.0 flutter: sdk: flutter + flutter_markdown: ^0.6.19 + google_generative_ai: ^0.2.0 + url_launcher: ^6.2.4 dev_dependencies: args: ^2.4.0 diff --git a/pkgs/samples/tool/samples.dart b/pkgs/samples/tool/samples.dart index a291b1b87..69ab7b89c 100644 --- a/pkgs/samples/tool/samples.dart +++ b/pkgs/samples/tool/samples.dart @@ -34,6 +34,7 @@ void main(List args) { const Set categories = { 'Dart', 'Flutter', + 'Ecosystem', }; class Samples { @@ -114,7 +115,7 @@ class Samples { stderr.writeln('Generated sample files not up-to-date.'); stderr.writeln('Re-generate by running:'); stderr.writeln(''); - stderr.writeln(' dart tool/samples.dart'); + stderr.writeln(' dart run tool/samples.dart'); stderr.writeln(''); exit(1); } @@ -154,12 +155,14 @@ import 'package:collection/collection.dart'; class Sample { final String category; + final String icon; final String name; final String id; final String source; - Sample({ + const Sample({ required this.category, + required this.icon, required this.name, required this.id, required this.source, @@ -171,12 +174,12 @@ class Sample { String toString() => '[\$category] \$name (\$id)'; } -class Samples { - static final List all = [ +abstract final class Samples { + static const List all = [ ${samples.map((s) => s.sourceId).join(',\n ')}, ]; - static final Map> categories = { + static const Map> categories = { ${categories.map((category) => _mapForCategory(category)).join(',\n ')}, }; @@ -187,7 +190,7 @@ class Samples { '''); - buf.writeln('Map _defaults = {'); + buf.writeln('const Map _defaults = {'); for (final entry in defaults.entries) { final source = File(entry.value).readAsStringSync().trimRight(); @@ -211,12 +214,14 @@ class Samples { class Sample implements Comparable { final String category; + final String icon; final String name; final String id; final String path; Sample({ required this.category, + required this.icon, required this.name, required this.id, required this.path, @@ -225,6 +230,7 @@ class Sample implements Comparable { factory Sample.fromJson(Map json) { return Sample( category: json['category'], + icon: json['icon'], name: json['name'], id: (json['id'] as String?) ?? _idFromName(json['name']), path: json['path'], @@ -246,8 +252,9 @@ class Sample implements Comparable { String get sourceDef { return ''' -final $sourceId = Sample( +const $sourceId = Sample( category: '$category', + icon: '$icon', name: '$name', id: '$id', source: r\'\'\' diff --git a/pkgs/sketch_pad/assets/dart_logo_128.png b/pkgs/sketch_pad/assets/dart_logo_128.png deleted file mode 100644 index 7ee61fd4ddedb6d521dce04741ca9c2768ead680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3081 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 Xog6#RHnIQh00000NkvXXu0mjf%OY^g diff --git a/pkgs/sketch_pad/assets/dart_logo_192.png b/pkgs/sketch_pad/assets/dart_logo_192.png new file mode 100644 index 0000000000000000000000000000000000000000..fb1d23f8a8e6215963d76f58becb3282b859b5ab GIT binary patch literal 4436 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 LGbPmOP-6ZM#dy8! literal 0 HcmV?d00001 diff --git a/pkgs/sketch_pad/assets/flame_logo_192.png b/pkgs/sketch_pad/assets/flame_logo_192.png new file mode 100644 index 0000000000000000000000000000000000000000..396099820cd4ad9dc41ea20b033a9eb796116a89 GIT binary patch literal 15606 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 { height: toolbarItemHeight, child: Row( children: [ - dartLogo(width: 32), + const Logo(width: 32, type: 'dart'), const SizedBox(width: denseSpacing), Text(appName, style: TextStyle( @@ -684,13 +684,17 @@ class SectionWidget extends StatelessWidget { class NewSnippetWidget extends StatelessWidget { final AppServices appServices; - static final _menuItems = [ + static const _menuItems = [ ( label: 'Dart snippet', - icon: dartLogo(), + icon: Logo(type: 'dart'), kind: 'dart', ), - (label: 'Flutter snippet', icon: flutterLogo(), kind: 'flutter'), + ( + label: 'Flutter snippet', + icon: Logo(type: 'flutter'), + kind: 'flutter', + ), ]; const NewSnippetWidget({ @@ -714,7 +718,7 @@ class NewSnippetWidget extends StatelessWidget { child: MenuItemButton( leadingIcon: item.icon, child: Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 32, 0), + padding: const EdgeInsets.only(right: 32), child: Text(item.label), ), onPressed: () => appServices.resetTo(type: item.kind), @@ -743,10 +747,9 @@ class ListSamplesWidget extends StatelessWidget { } List _buildMenuItems(BuildContext context) { - final categories = Samples.categories.keys; - final menuItems = [ - for (final category in categories) ...[ + for (final MapEntry(key: category, value: samples) + in Samples.categories.entries) ...[ MenuItemButton( onPressed: null, child: Text( @@ -754,13 +757,13 @@ class ListSamplesWidget extends StatelessWidget { style: Theme.of(context).textTheme.bodyLarge, ), ), - for (final sample in Samples.categories[category]!) + for (final sample in samples) MenuItemButton( - leadingIcon: sample.isDart ? dartLogo() : flutterLogo(), + leadingIcon: Logo(type: sample.icon), onPressed: () => GoRouter.of(context).replaceQueryParam('sample', sample.id), child: Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 32, 0), + padding: const EdgeInsets.only(right: 32), child: Text(sample.name), ), ), diff --git a/pkgs/sketch_pad/lib/samples.g.dart b/pkgs/sketch_pad/lib/samples.g.dart index 27cb03e4a..4f88ac2c7 100644 --- a/pkgs/sketch_pad/lib/samples.g.dart +++ b/pkgs/sketch_pad/lib/samples.g.dart @@ -8,12 +8,14 @@ import 'package:collection/collection.dart'; class Sample { final String category; + final String icon; final String name; final String id; final String source; - Sample({ + const Sample({ required this.category, + required this.icon, required this.name, required this.id, required this.source, @@ -25,15 +27,17 @@ class Sample { String toString() => '[$category] $name ($id)'; } -class Samples { - static final List all = [ +abstract final class Samples { + static const List all = [ _fibonacci, _helloWorld, + _flameGame, + _googleSdk, _counter, _sunflower, ]; - static final Map> categories = { + static const Map> categories = { 'Dart': [ _fibonacci, _helloWorld, @@ -42,6 +46,10 @@ class Samples { _counter, _sunflower, ], + 'Ecosystem': [ + _flameGame, + _googleSdk, + ], }; static Sample? getById(String? id) => all.firstWhereOrNull((s) => s.id == id); @@ -49,7 +57,7 @@ class Samples { static String getDefault({required String type}) => _defaults[type]!; } -Map _defaults = { +const Map _defaults = { 'dart': r''' void main() { for (int i = 0; i < 10; i++) { @@ -82,8 +90,9 @@ class MyApp extends StatelessWidget { ''', }; -final _fibonacci = Sample( +const _fibonacci = Sample( category: 'Dart', + icon: 'dart', name: 'Fibonacci', id: 'fibonacci', source: r''' @@ -104,8 +113,9 @@ int fibonacci(int n) { ''', ); -final _helloWorld = Sample( +const _helloWorld = Sample( category: 'Dart', + icon: 'dart', name: 'Hello world', id: 'hello-world', source: r''' @@ -121,8 +131,692 @@ void main() { ''', ); -final _counter = Sample( +const _flameGame = Sample( + category: 'Ecosystem', + icon: 'flame', + name: 'Flame game', + id: 'flame-game', + source: r''' +// Copyright 2024 the Dart project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +/// A simplified brick-breaker game, +/// built using the Flame game engine for Flutter. +/// +/// To learn how to build a more complete version of this game yourself, +/// check out the codelab at https://docs.flutter.dev/brick-breaker. +library; + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() { + runApp(const GameApp()); +} + +class GameApp extends StatefulWidget { + const GameApp({super.key}); + + @override + State createState() => _GameAppState(); +} + +class _GameAppState extends State { + late final BrickBreaker game; + + @override + void initState() { + super.initState(); + game = BrickBreaker(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xffa9d6e5), + Color(0xfff2e8cf), + ], + ), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: FittedBox( + child: SizedBox( + width: gameWidth, + height: gameHeight, + child: GameWidget( + game: game, + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} + +class BrickBreaker extends FlameGame + with HasCollisionDetection, KeyboardEvents, TapDetector { + BrickBreaker() + : super( + camera: CameraComponent.withFixedResolution( + width: gameWidth, height: gameHeight)); + + final rand = math.Random(); + double get width => size.x; + double get height => size.y; + + @override + FutureOr onLoad() async { + super.onLoad(); + camera.viewfinder.anchor = Anchor.topLeft; + world.add(PlayArea()); + startGame(); + } + + void startGame() { + world.removeAll(world.children.query()); + world.removeAll(world.children.query()); + world.removeAll(world.children.query()); + + world.add(Ball( + difficultyModifier: difficultyModifier, + radius: ballRadius, + position: size / 2, + velocity: + Vector2((rand.nextDouble() - 0.5) * width, height * 0.2).normalized() + ..scale(height / 4), + )); + + world.add(Bat( + size: Vector2(batWidth, batHeight), + cornerRadius: const Radius.circular(ballRadius / 2), + position: Vector2(width / 2, height * 0.95), + )); + + world.addAll([ + for (var i = 0; i < brickColors.length; i++) + for (var j = 1; j <= 5; j++) + Brick( + Vector2( + (i + 0.5) * brickWidth + (i + 1) * brickGutter, + (j + 2.0) * brickHeight + j * brickGutter, + ), + brickColors[i], + ), + ]); + } + + @override + void onTap() { + super.onTap(); + startGame(); + } + + @override + KeyEventResult onKeyEvent( + RawKeyEvent event, Set keysPressed) { + super.onKeyEvent(event, keysPressed); + switch (event.logicalKey) { + case LogicalKeyboardKey.arrowLeft: + case LogicalKeyboardKey.keyA: + world.children.query().first.moveBy(-batStep); + case LogicalKeyboardKey.keyD: + case LogicalKeyboardKey.arrowRight: + world.children.query().first.moveBy(batStep); + case LogicalKeyboardKey.space: + case LogicalKeyboardKey.enter: + startGame(); + } + return KeyEventResult.handled; + } + + @override + Color backgroundColor() => const Color(0xfff2e8cf); +} + +class Ball extends CircleComponent + with CollisionCallbacks, HasGameReference { + Ball({ + required this.velocity, + required super.position, + required double radius, + required this.difficultyModifier, + }) : super( + radius: radius, + anchor: Anchor.center, + paint: Paint() + ..color = const Color(0xff1e6091) + ..style = PaintingStyle.fill, + children: [CircleHitbox()]); + + final Vector2 velocity; + final double difficultyModifier; + + @override + void update(double dt) { + super.update(dt); + position += velocity * dt; + } + + @override + void onCollisionStart( + Set intersectionPoints, PositionComponent other) { + super.onCollisionStart(intersectionPoints, other); + if (other is PlayArea) { + if (intersectionPoints.first.y <= 0) { + velocity.y = -velocity.y; + } else if (intersectionPoints.first.x <= 0) { + velocity.x = -velocity.x; + } else if (intersectionPoints.first.x >= game.width) { + velocity.x = -velocity.x; + } else if (intersectionPoints.first.y >= game.height) { + add(RemoveEffect( + delay: 0.35, + onComplete: () { + game.startGame(); + }, + )); + } + } else if (other is Bat) { + velocity.y = -velocity.y; + velocity.x = velocity.x + + (position.x - other.position.x) / other.size.x * game.width * 0.3; + } else if (other is Brick) { + if (position.y < other.position.y - other.size.y / 2) { + velocity.y = -velocity.y; + } else if (position.y > other.position.y + other.size.y / 2) { + velocity.y = -velocity.y; + } else if (position.x < other.position.x) { + velocity.x = -velocity.x; + } else if (position.x > other.position.x) { + velocity.x = -velocity.x; + } + velocity.setFrom(velocity * difficultyModifier); + } + } +} + +class Bat extends PositionComponent + with DragCallbacks, HasGameReference { + Bat({ + required this.cornerRadius, + required super.position, + required super.size, + }) : super(anchor: Anchor.center, children: [RectangleHitbox()]); + + final Radius cornerRadius; + + final _paint = Paint() + ..color = const Color(0xff1e6091) + ..style = PaintingStyle.fill; + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas.drawRRect( + RRect.fromRectAndRadius( + Offset.zero & size.toSize(), + cornerRadius, + ), + _paint, + ); + } + + @override + void onDragUpdate(DragUpdateEvent event) { + if (isRemoved) return; + super.onDragUpdate(event); + position.x = (position.x + event.localDelta.x) + .clamp(width / 2, game.width - width / 2); + } + + void moveBy(double dx) { + add(MoveToEffect( + Vector2( + (position.x + dx).clamp(width / 2, game.width - width / 2), + position.y, + ), + EffectController(duration: 0.1), + )); + } +} + +class Brick extends RectangleComponent + with CollisionCallbacks, HasGameReference { + Brick(Vector2 position, Color color) + : super( + position: position, + size: Vector2(brickWidth, brickHeight), + anchor: Anchor.center, + paint: Paint() + ..color = color + ..style = PaintingStyle.fill, + children: [RectangleHitbox()], + ); + + @override + void onCollisionStart( + Set intersectionPoints, PositionComponent other) { + super.onCollisionStart(intersectionPoints, other); + removeFromParent(); + + if (game.world.children.query().length == 1) { + game.startGame(); + } + } +} + +class PlayArea extends RectangleComponent with HasGameReference { + PlayArea() : super(children: [RectangleHitbox()]); + + @override + Future onLoad() async { + super.onLoad(); + size = Vector2(game.width, game.height); + } +} + +const brickColors = [ + Color(0xfff94144), + Color(0xfff3722c), + Color(0xfff8961e), + Color(0xfff9844a), + Color(0xfff9c74f), + Color(0xff90be6d), + Color(0xff43aa8b), + Color(0xff4d908e), + Color(0xff277da1), + Color(0xff577590), +]; + +const gameWidth = 820.0; +const gameHeight = 1600.0; +const ballRadius = gameWidth * 0.02; +const batWidth = gameWidth * 0.2; +const batHeight = ballRadius * 2; +const batStep = gameWidth * 0.05; +const brickGutter = gameWidth * 0.015; +final brickWidth = + (gameWidth - (brickGutter * (brickColors.length + 1))) / brickColors.length; +const brickHeight = gameHeight * 0.03; +const difficultyModifier = 1.05; +''', +); + +const _googleSdk = Sample( + category: 'Ecosystem', + icon: 'gemini', + name: 'Google AI SDK', + id: 'google-ai-sdk', + source: r''' +// Copyright 2024 the Dart project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:google_generative_ai/google_generative_ai.dart'; +import 'package:url_launcher/link.dart'; + +void main() { + runApp(const GenerativeAISample()); +} + +class GenerativeAISample extends StatelessWidget { + const GenerativeAISample({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Flutter + Generative AI', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + brightness: Brightness.dark, + seedColor: const Color.fromARGB(255, 171, 222, 244), + ), + useMaterial3: true, + ), + home: const ChatScreen(title: 'Flutter + Generative AI'), + ); + } +} + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key, required this.title}); + + final String title; + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + String? apiKey; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: switch (apiKey) { + final providedKey? => ChatWidget(apiKey: providedKey), + _ => ApiKeyWidget(onSubmitted: (key) { + setState(() => apiKey = key); + }), + }, + ); + } +} + +class ApiKeyWidget extends StatelessWidget { + ApiKeyWidget({required this.onSubmitted, super.key}); + + final ValueChanged onSubmitted; + final TextEditingController _textController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'To use the Gemini API, you\'ll need an API key. ' + 'If you don\'t already have one, ' + 'create a key in Google AI Studio.', + ), + const SizedBox(height: 8), + Link( + uri: Uri.https('makersuite.google.com', '/app/apikey'), + target: LinkTarget.blank, + builder: (context, followLink) => TextButton( + onPressed: followLink, + child: const Text('Get an API Key'), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextField( + decoration: + textFieldDecoration(context, 'Enter your API key'), + controller: _textController, + onSubmitted: (value) { + onSubmitted(value); + }, + ), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () { + onSubmitted(_textController.value.text); + }, + child: const Text('Submit'), + ), + ], + ), + ], + ), + ), + ); + } +} + +class ChatWidget extends StatefulWidget { + const ChatWidget({required this.apiKey, super.key}); + + final String apiKey; + + @override + State createState() => _ChatWidgetState(); +} + +class _ChatWidgetState extends State { + late final GenerativeModel _model; + late final ChatSession _chat; + final ScrollController _scrollController = ScrollController(); + final TextEditingController _textController = TextEditingController(); + final FocusNode _textFieldFocus = FocusNode(debugLabel: 'TextField'); + bool _loading = false; + + @override + void initState() { + super.initState(); + _model = GenerativeModel( + model: 'gemini-pro', + apiKey: widget.apiKey, + ); + _chat = _model.startChat(); + } + + void _scrollDown() { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration( + milliseconds: 750, + ), + curve: Curves.easeOutCirc, + ), + ); + } + + @override + Widget build(BuildContext context) { + final history = _chat.history.toList(); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemBuilder: (context, idx) { + final content = history[idx]; + final text = content.parts + .whereType() + .map((e) => e.text) + .join(''); + return MessageWidget( + text: text, + isFromUser: content.role == 'user', + ); + }, + itemCount: history.length, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 25, + horizontal: 15, + ), + child: Row( + children: [ + Expanded( + child: TextField( + autofocus: true, + focusNode: _textFieldFocus, + decoration: + textFieldDecoration(context, 'Enter a prompt...'), + controller: _textController, + onSubmitted: (String value) { + _sendChatMessage(value); + }, + ), + ), + const SizedBox.square(dimension: 15), + if (!_loading) + IconButton( + onPressed: () async { + _sendChatMessage(_textController.text); + }, + icon: Icon( + Icons.send, + color: Theme.of(context).colorScheme.primary, + ), + ) + else + const CircularProgressIndicator(), + ], + ), + ), + ], + ), + ); + } + + Future _sendChatMessage(String message) async { + setState(() { + _loading = true; + }); + + try { + final response = await _chat.sendMessage( + Content.text(message), + ); + final text = response.text; + + if (text == null) { + _showError('Empty response.'); + return; + } else { + setState(() { + _loading = false; + _scrollDown(); + }); + } + } catch (e) { + _showError(e.toString()); + setState(() { + _loading = false; + }); + } finally { + _textController.clear(); + setState(() { + _loading = false; + }); + _textFieldFocus.requestFocus(); + } + } + + void _showError(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Something went wrong'), + content: SingleChildScrollView( + child: Text(message), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ) + ], + ); + }, + ); + } +} + +class MessageWidget extends StatelessWidget { + const MessageWidget({ + super.key, + required this.text, + required this.isFromUser, + }); + + final String text; + final bool isFromUser; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: + isFromUser ? MainAxisAlignment.end : MainAxisAlignment.start, + children: [ + Flexible( + child: Container( + constraints: const BoxConstraints(maxWidth: 480), + decoration: BoxDecoration( + color: isFromUser + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(18), + ), + padding: const EdgeInsets.symmetric( + vertical: 15, + horizontal: 20, + ), + margin: const EdgeInsets.only(bottom: 8), + child: MarkdownBody(data: text), + ), + ), + ], + ); + } +} + +InputDecoration textFieldDecoration(BuildContext context, String hintText) => + InputDecoration( + contentPadding: const EdgeInsets.all(15), + hintText: hintText, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(14), + ), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(14), + ), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ); +''', +); + +const _counter = Sample( category: 'Flutter', + icon: 'flutter', name: 'Counter', id: 'counter', source: r''' @@ -202,8 +896,9 @@ class _MyHomePageState extends State { ''', ); -final _sunflower = Sample( +const _sunflower = Sample( category: 'Flutter', + icon: 'flutter', name: 'Sunflower', id: 'sunflower', source: r''' diff --git a/pkgs/sketch_pad/lib/utils.dart b/pkgs/sketch_pad/lib/utils.dart index 2dc0fb9f8..fa62a3b86 100644 --- a/pkgs/sketch_pad/lib/utils.dart +++ b/pkgs/sketch_pad/lib/utils.dart @@ -28,16 +28,6 @@ void unimplemented(BuildContext context, String message) { String generateSnippetName() => fluttering_phrases.generate(); -Image dartLogo({double? width}) { - return Image.asset('assets/dart_logo_128.png', - width: width ?? defaultIconSize); -} - -Image flutterLogo({double? width}) { - return Image.asset('assets/flutter_logo_192.png', - width: width ?? defaultIconSize); -} - RelativeRect calculatePopupMenuPosition( BuildContext context, { bool growUpwards = false, diff --git a/pkgs/sketch_pad/lib/widgets.dart b/pkgs/sketch_pad/lib/widgets.dart index 87821a3bb..8ca68756a 100644 --- a/pkgs/sketch_pad/lib/widgets.dart +++ b/pkgs/sketch_pad/lib/widgets.dart @@ -248,3 +248,22 @@ class GoldenRatioCenter extends StatelessWidget { ); } } + +final class Logo extends StatelessWidget { + final String? _type; + final double width; + + const Logo({super.key, this.width = defaultIconSize, String? type}) + : _type = type; + + @override + Widget build(BuildContext context) { + final assetPath = switch (_type) { + 'flutter' => 'assets/flutter_logo_192.png', + 'flame' => 'assets/flame_logo_192.png', + 'gemini' => 'assets/gemini_sparkle_192.png', + _ => 'assets/dart_logo_192.png', + }; + return Image.asset(assetPath, width: width); + } +} diff --git a/pkgs/sketch_pad/pubspec.yaml b/pkgs/sketch_pad/pubspec.yaml index 77f932a05..6f27b9998 100644 --- a/pkgs/sketch_pad/pubspec.yaml +++ b/pkgs/sketch_pad/pubspec.yaml @@ -32,8 +32,10 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/dart_logo_128.png + - assets/dart_logo_192.png + - assets/flame_logo_192.png - assets/flutter_logo_192.png + - assets/gemini_sparkle_192.png fonts: - family: RobotoMono fonts: