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 000000000..d3a80071b Binary files /dev/null and b/pkgs/dartpad_ui/assets/idx_192.png differ diff --git a/pkgs/dartpad_ui/lib/main.dart b/pkgs/dartpad_ui/lib/main.dart index ca80cdea2..d554eab39 100644 --- a/pkgs/dartpad_ui/lib/main.dart +++ b/pkgs/dartpad_ui/lib/main.dart @@ -703,19 +703,8 @@ 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( @@ -732,6 +721,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 { @@ -1110,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 @@ -1157,6 +1145,38 @@ class OverflowMenu extends StatelessWidget { } } +class ContinueInMenu extends StatelessWidget { + final VoidCallback openInIdx; + const ContinueInMenu({super.key, required this.openInIdx}); + + @override + Widget build(BuildContext context) { + return MenuAnchor( + builder: (context, MenuController controller, Widget? child) { + return TextButton.icon( + onPressed: () => controller.toggleMenuState(), + icon: const Icon(Icons.file_download_outlined), + label: const Text('Open in'), + ); + }, + menuChildren: [ + ...[ + 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)) + ], + ); + } +} + class KeyBindingsTable extends StatelessWidget { final List<(String, List)> bindings; 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'), ); 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'), );