From d958acf47500fd254b3174b5072cbd76ad32b88f Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 12 Jun 2024 09:31:58 -0700 Subject: [PATCH 1/5] Add Open in IDX feature --- pkgs/dart_services/lib/src/common_server.dart | 30 ++++++++ .../lib/src/common_server.g.dart | 5 ++ pkgs/dartpad_shared/lib/model.dart | 34 ++++++++++ pkgs/dartpad_shared/lib/model.g.dart | 20 ++++++ pkgs/dartpad_shared/lib/services.dart | 3 + pkgs/dartpad_ui/assets/idx_192.png | Bin 0 -> 1228 bytes pkgs/dartpad_ui/lib/main.dart | 64 ++++++++++++++---- pkgs/dartpad_ui/lib/widgets.dart | 1 + pkgs/dartpad_ui/pubspec.yaml | 1 + pkgs/samples/lib/main.dart | 2 +- 10 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 pkgs/dartpad_ui/assets/idx_192.png diff --git a/pkgs/dart_services/lib/src/common_server.dart b/pkgs/dart_services/lib/src/common_server.dart index 0a3246277..79a27a308 100644 --- a/pkgs/dart_services/lib/src/common_server.dart +++ b/pkgs/dart_services/lib/src/common_server.dart @@ -202,6 +202,36 @@ class CommonServerApi { return ok(version().toJson()); } + @Route.post('$apiPrefix/openInIDX') + Future openInIdx(Request request, String apiVersion) async { + final code = api.OpenInIdxRequest.fromJson(await request.readAsJson()).code; + final idxUrl = Uri.parse('https://idx.google.com/run.api'); + + final data = { + 'project[files][lib/main.dart]': code, + 'project[settings]': '{"baselineEnvironment": "flutter"}', + }; + try { + final response = await http.post( + idxUrl, + body: data, + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + ); + + if (response.statusCode == 302) { + return ok(api.OpenInIdxResponse(idxUrl: response.headers['location']!) + .toJson()); + } else { + return Response.internalServerError( + body: + 'Failed to read response from IDX server. Response: $response'); + } + } catch (error) { + return Response.internalServerError( + body: 'Failed to read response from IDX server. Error: $error'); + } + } + static final String? geminiApiKey = Platform.environment['GEMINI_API_KEY']; http.Client? geminiHttpClient; diff --git a/pkgs/dart_services/lib/src/common_server.g.dart b/pkgs/dart_services/lib/src/common_server.g.dart index fc1bf9250..71da0773f 100644 --- a/pkgs/dart_services/lib/src/common_server.g.dart +++ b/pkgs/dart_services/lib/src/common_server.g.dart @@ -48,6 +48,11 @@ Router _$CommonServerApiRouter(CommonServerApi service) { r'/api//version', service.versionGet, ); + router.add( + 'POST', + r'/api//openInIDX', + service.openInIdx, + ); router.add( 'POST', r'/api//_gemini', diff --git a/pkgs/dartpad_shared/lib/model.dart b/pkgs/dartpad_shared/lib/model.dart index 107a5bb50..43787e5a2 100644 --- a/pkgs/dartpad_shared/lib/model.dart +++ b/pkgs/dartpad_shared/lib/model.dart @@ -392,6 +392,40 @@ class GeminiResponse { String toString() => 'GeminiResponse[response=$response]'; } +@JsonSerializable() +class OpenInIdxRequest { + final String code; + + OpenInIdxRequest({ + required this.code, + }); + + factory OpenInIdxRequest.fromJson(Map json) => + _$OpenInIdxRequestFromJson(json); + + Map toJson() => _$OpenInIdxRequestToJson(this); + + @override + String toString() => 'OpenInIdxRequest [${code.substring(0, 10)} (...)'; +} + +@JsonSerializable() +class OpenInIdxResponse { + final String idxUrl; + + OpenInIdxResponse({ + required this.idxUrl, + }); + + factory OpenInIdxResponse.fromJson(Map json) => + _$OpenInIdxResponseFromJson(json); + + Map toJson() => _$OpenInIdxResponseToJson(this); + + @override + String toString() => 'OpenInIdxResponse [$idxUrl]'; +} + @JsonSerializable() class PackageInfo { final String name; diff --git a/pkgs/dartpad_shared/lib/model.g.dart b/pkgs/dartpad_shared/lib/model.g.dart index ece3f3dcb..507e7ac27 100644 --- a/pkgs/dartpad_shared/lib/model.g.dart +++ b/pkgs/dartpad_shared/lib/model.g.dart @@ -306,6 +306,26 @@ Map _$GeminiResponseToJson(GeminiResponse instance) => 'response': instance.response, }; +OpenInIdxRequest _$OpenInIdxRequestFromJson(Map json) => + OpenInIdxRequest( + code: json['code'] as String, + ); + +Map _$OpenInIdxRequestToJson(OpenInIdxRequest instance) => + { + 'code': instance.code, + }; + +OpenInIdxResponse _$OpenInIdxResponseFromJson(Map json) => + OpenInIdxResponse( + idxUrl: json['idxUrl'] as String, + ); + +Map _$OpenInIdxResponseToJson(OpenInIdxResponse instance) => + { + 'idxUrl': instance.idxUrl, + }; + PackageInfo _$PackageInfoFromJson(Map json) => PackageInfo( name: json['name'] as String, version: json['version'] as String, diff --git a/pkgs/dartpad_shared/lib/services.dart b/pkgs/dartpad_shared/lib/services.dart index 71c024a50..886f6b7eb 100644 --- a/pkgs/dartpad_shared/lib/services.dart +++ b/pkgs/dartpad_shared/lib/services.dart @@ -41,6 +41,9 @@ class ServicesClient { Future compileDDC(CompileRequest request) => _requestPost('compileDDC', request.toJson(), CompileDDCResponse.fromJson); + Future openInIdx(OpenInIdxRequest request) => + _requestPost('openInIDX', request.toJson(), OpenInIdxResponse.fromJson); + /// Note: this API is experimental and could change or be removed at any time. @experimental Future gemini(SourceRequest request) => diff --git a/pkgs/dartpad_ui/assets/idx_192.png b/pkgs/dartpad_ui/assets/idx_192.png new file mode 100644 index 0000000000000000000000000000000000000000..d3a80071ba643b9748c13a3e657acab8641a953f GIT binary patch literal 1228 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1uL68RT-^(N8Vc@rr@ZV+dC{5t zqBG@1SIW!olo#z8&%07zcBQ@oih$9}hJ^c_DbG7oU$mz@?@xH&mhue9Rak#tZTV}} z<*(G1zf@iJQg!Jo^<}R$mOlrIsIPbh6j5LH0w|@n6eOz#L`$D5%zv-4?3v247b;7i ztANG-uXp&r*zW&g`~Qo8$PSEb|1Wp=zr-FSz7ULn;`1Fq2&iVEox=JCv7Ke&JIbMG zCy-GlvAseJD6*>@$dK4kA+WyyNQ!R*i31r4(GRBpeeG8g*{qOlz9{c1h1fP6({Az2-^eI7JnoFPDDzjda6#jKd;Q>1@)4f?& z^CjLee_d35?2GiV-xAk{iw9_&73Dto&>-YCeVAJS)@B6_%)&&U64Bq6NmoKj7 z&Qa{iJm#>w+4$nemb7%W+os3ozx2xxIahMrVg5u`p4=u?wi3&YM_6NfRC!|OsIGf) zNa@#s^t%y{R;c}3W9>cX!qbC7jr(JrAIsZ6v$^o%WQlOpS)XkSZeE^wLVZj3)J&ZX ztrjysAOB{?Us0(91p7}}|1Qa6P!u>EaD~;-&FuTd`dDedZOZCh6&vl$gFj2ww>~#| z_1i6{FyLD1h3~yr#1vfle(e*?(#UYR?~~>1DO5Y_f{;chNbXpdl6sZ-Ox9H4i~h?d zY25CtGZ%HADZF>~sm4u{U-$@M?|MmX4rRf_F{+>4bjC}tdMajKikDUR6hRfH=YA@fOQByzR z=9lH4S$;Fe?)rC;{r>aYTQ+>G-PS#g?QZ()Snt|RwYxSm^}gNg+v=AxKTo4X^>>Ny zLbq=(cR5+#Ts}D|LH-l>4bfLA^VeyXDE}@AZ)Ll=e7CmANxS&q2aVtCW_H_dHF>e` zM|sWD-!XeYm{vNIO69b>x8m-#r?)H=QfSlylhw_GRAVRy`-9n-2?N*?)CO<`G0^JmBG{1&t;ucLK6TFpK`MR literal 0 HcmV?d00001 diff --git a/pkgs/dartpad_ui/lib/main.dart b/pkgs/dartpad_ui/lib/main.dart index ca80cdea2..ab5ce53ba 100644 --- a/pkgs/dartpad_ui/lib/main.dart +++ b/pkgs/dartpad_ui/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:provider/provider.dart'; import 'package:split_view/split_view.dart'; @@ -703,20 +704,7 @@ class DartPadAppBar extends StatelessWidget implements PreferredSizeWidget { actions: [ // Hide the Install SDK button when the screen width is too small. if (constraints.maxWidth > smallScreenWidth) - TextButton( - onPressed: () { - url_launcher.launchUrl( - Uri.parse('https://docs.flutter.dev/get-started/install'), - ); - }, - child: const Row( - children: [ - Text('Install SDK'), - SizedBox(width: denseSpacing), - Icon(Icons.launch, size: 18), - ], - ), - ), + ContinueInMenu(openInIdx: _openInIDX,), const SizedBox(width: denseSpacing), _BrightnessButton( handleBrightnessChange: widget.handleBrightnessChanged, @@ -732,6 +720,13 @@ class DartPadAppBar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => bottom == null ? const Size(double.infinity, 56.0) : const Size(double.infinity, 112.0); + + Future _openInIDX() async { + final code = appModel.sourceCodeController.text; + final request = OpenInIdxRequest(code: code); + final response = await appServices.services.openInIdx(request); + url_launcher.launchUrl(Uri.parse(response.idxUrl)); + } } class EditorWithButtons extends StatelessWidget { @@ -1157,6 +1152,47 @@ class OverflowMenu extends StatelessWidget { } } +class ContinueInMenu extends StatelessWidget { + VoidCallback openInIdx; + ContinueInMenu({super.key, required this.openInIdx}); + + @override + Widget build(BuildContext context) { + return MenuAnchor( + builder: (context, MenuController controller, Widget? child) { + return TextButton( + child: const Text('Continue in...'), + onPressed: () => controller.toggleMenuState(), + ); + }, + menuChildren: [ + ...[ + MenuItemButton( + trailingIcon: const Logo(type: 'idx'), + onPressed: () { + openInIdx(); + }, + child: const Padding( + padding: EdgeInsets.fromLTRB(0, 0, 32, 0), + child: Text('IDX'), + ), + ), + MenuItemButton( + trailingIcon: const Icon(Icons.launch), + onPressed: () { + url_launcher.launchUrl(Uri.parse('https://docs.flutter.dev/get-started/install')); + }, + child: const Padding( + padding: EdgeInsets.fromLTRB(0, 0, 32, 0), + child: Text('Install SDK'), + ), + )].map((widget) => PointerInterceptor(child: widget)) + ], + ); + } +} + + class KeyBindingsTable extends StatelessWidget { final List<(String, List)> bindings; diff --git a/pkgs/dartpad_ui/lib/widgets.dart b/pkgs/dartpad_ui/lib/widgets.dart index 542e41db0..6f6d784cc 100644 --- a/pkgs/dartpad_ui/lib/widgets.dart +++ b/pkgs/dartpad_ui/lib/widgets.dart @@ -261,6 +261,7 @@ final class Logo extends StatelessWidget { 'flutter' => 'assets/flutter_logo_192.png', 'flame' => 'assets/flame_logo_192.png', 'gemini' => 'assets/gemini_sparkle_192.png', + 'idx' => 'assets/idx_192.png', _ => 'assets/dart_logo_192.png', }; return Image.asset(assetPath, width: width); diff --git a/pkgs/dartpad_ui/pubspec.yaml b/pkgs/dartpad_ui/pubspec.yaml index 7a0369a1d..675ece2a1 100644 --- a/pkgs/dartpad_ui/pubspec.yaml +++ b/pkgs/dartpad_ui/pubspec.yaml @@ -38,6 +38,7 @@ flutter: - assets/flame_logo_192.png - assets/flutter_logo_192.png - assets/gemini_sparkle_192.png + - assets/idx_192.png - assets/RobotoMono-Bold.ttf - assets/RobotoMono-Regular.ttf diff --git a/pkgs/samples/lib/main.dart b/pkgs/samples/lib/main.dart index 12bbdd4bd..9c417491e 100644 --- a/pkgs/samples/lib/main.dart +++ b/pkgs/samples/lib/main.dart @@ -15,7 +15,7 @@ class MyApp extends StatelessWidget { title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( - primarySwatch: Colors.blue, + colorSchemeSeed: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); From a19cce55d936efcbc707f03fdce89f7d3c4ff4ac Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 12 Jun 2024 09:37:06 -0700 Subject: [PATCH 2/5] fix warnings --- pkgs/dartpad_ui/lib/main.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkgs/dartpad_ui/lib/main.dart b/pkgs/dartpad_ui/lib/main.dart index ab5ce53ba..158d73183 100644 --- a/pkgs/dartpad_ui/lib/main.dart +++ b/pkgs/dartpad_ui/lib/main.dart @@ -10,7 +10,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:http/http.dart' as http; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:provider/provider.dart'; import 'package:split_view/split_view.dart'; @@ -1153,8 +1152,8 @@ class OverflowMenu extends StatelessWidget { } class ContinueInMenu extends StatelessWidget { - VoidCallback openInIdx; - ContinueInMenu({super.key, required this.openInIdx}); + final VoidCallback openInIdx; + const ContinueInMenu({super.key, required this.openInIdx}); @override Widget build(BuildContext context) { From 18ca1f84c7d110f5035582310d73ae33a5d06f55 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 12 Jun 2024 09:43:52 -0700 Subject: [PATCH 3/5] format --- pkgs/dartpad_ui/lib/main.dart | 45 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pkgs/dartpad_ui/lib/main.dart b/pkgs/dartpad_ui/lib/main.dart index 158d73183..aa38ce65b 100644 --- a/pkgs/dartpad_ui/lib/main.dart +++ b/pkgs/dartpad_ui/lib/main.dart @@ -703,7 +703,9 @@ class DartPadAppBar extends StatelessWidget implements PreferredSizeWidget { actions: [ // Hide the Install SDK button when the screen width is too small. if (constraints.maxWidth > smallScreenWidth) - ContinueInMenu(openInIdx: _openInIDX,), + ContinueInMenu( + openInIdx: _openInIDX, + ), const SizedBox(width: denseSpacing), _BrightnessButton( handleBrightnessChange: widget.handleBrightnessChanged, @@ -1166,32 +1168,33 @@ class ContinueInMenu extends StatelessWidget { }, menuChildren: [ ...[ - MenuItemButton( - trailingIcon: const Logo(type: 'idx'), - onPressed: () { - openInIdx(); - }, - child: const Padding( - padding: EdgeInsets.fromLTRB(0, 0, 32, 0), - child: Text('IDX'), - ), - ), - MenuItemButton( - trailingIcon: const Icon(Icons.launch), - onPressed: () { - url_launcher.launchUrl(Uri.parse('https://docs.flutter.dev/get-started/install')); - }, - child: const Padding( - padding: EdgeInsets.fromLTRB(0, 0, 32, 0), - child: Text('Install SDK'), + MenuItemButton( + trailingIcon: const Logo(type: 'idx'), + onPressed: () { + openInIdx(); + }, + child: const Padding( + padding: EdgeInsets.fromLTRB(0, 0, 32, 0), + child: Text('IDX'), + ), ), - )].map((widget) => PointerInterceptor(child: widget)) + MenuItemButton( + trailingIcon: const Icon(Icons.launch), + onPressed: () { + url_launcher.launchUrl( + Uri.parse('https://docs.flutter.dev/get-started/install')); + }, + child: const Padding( + padding: EdgeInsets.fromLTRB(0, 0, 32, 0), + child: Text('Install SDK'), + ), + ) + ].map((widget) => PointerInterceptor(child: widget)) ], ); } } - class KeyBindingsTable extends StatelessWidget { final List<(String, List)> bindings; From f6e74a39c89a8b1b3708fe0c6e41aedafaefdab0 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 12 Jun 2024 09:44:51 -0700 Subject: [PATCH 4/5] use colorSchemeSeed in sample --- pkgs/dartpad_ui/lib/samples.g.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/dartpad_ui/lib/samples.g.dart b/pkgs/dartpad_ui/lib/samples.g.dart index acdeca93c..cea769551 100644 --- a/pkgs/dartpad_ui/lib/samples.g.dart +++ b/pkgs/dartpad_ui/lib/samples.g.dart @@ -839,7 +839,7 @@ class MyApp extends StatelessWidget { title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( - primarySwatch: Colors.blue, + colorSchemeSeed: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); From 09cf0f70e68bf57aeedf8a7ac6bdff2f2e8de675 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Wed, 12 Jun 2024 10:57:04 -0700 Subject: [PATCH 5/5] Change to Open in and add icon --- pkgs/dartpad_ui/lib/main.dart | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/pkgs/dartpad_ui/lib/main.dart b/pkgs/dartpad_ui/lib/main.dart index aa38ce65b..d554eab39 100644 --- a/pkgs/dartpad_ui/lib/main.dart +++ b/pkgs/dartpad_ui/lib/main.dart @@ -1106,21 +1106,13 @@ class OverflowMenu extends StatelessWidget { static const _menuItems = [ ( - label: 'dart.dev', - uri: 'https://dart.dev', - ), - ( - label: 'flutter.dev', - uri: 'https://flutter.dev', + label: 'Install SDK', + uri: 'https://docs.flutter.dev/get-started/install', ), ( label: 'Sharing guide', uri: 'https://github.com/dart-lang/dart-pad/wiki/Sharing-Guide' ), - ( - label: 'DartPad on GitHub', - uri: 'https://github.com/dart-lang/dart-pad', - ), ]; @override @@ -1161,9 +1153,10 @@ class ContinueInMenu extends StatelessWidget { Widget build(BuildContext context) { return MenuAnchor( builder: (context, MenuController controller, Widget? child) { - return TextButton( - child: const Text('Continue in...'), + return TextButton.icon( onPressed: () => controller.toggleMenuState(), + icon: const Icon(Icons.file_download_outlined), + label: const Text('Open in'), ); }, menuChildren: [ @@ -1178,17 +1171,6 @@ class ContinueInMenu extends StatelessWidget { child: Text('IDX'), ), ), - MenuItemButton( - trailingIcon: const Icon(Icons.launch), - onPressed: () { - url_launcher.launchUrl( - Uri.parse('https://docs.flutter.dev/get-started/install')); - }, - child: const Padding( - padding: EdgeInsets.fromLTRB(0, 0, 32, 0), - child: Text('Install SDK'), - ), - ) ].map((widget) => PointerInterceptor(child: widget)) ], );