From b3d6ef14321bb5e405854e8ab2d79a2c771ee0de Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 27 Oct 2023 12:57:53 -0700 Subject: [PATCH] Expose a keyboard shortcut to copy the Dart app ID (#2271) --- dwds/debug_extension_mv3/web/background.dart | 23 ++++- dwds/debug_extension_mv3/web/chrome_api.dart | 24 +++++- dwds/debug_extension_mv3/web/copier.dart | 55 ++++++++++++ .../debug_extension_mv3/web/manifest_mv2.json | 12 ++- .../debug_extension_mv3/web/manifest_mv3.json | 12 ++- dwds/debug_extension_mv3/web/messaging.dart | 86 +++++++++++++++---- .../web/static_assets/styles.css | 10 ++- 7 files changed, 197 insertions(+), 25 deletions(-) create mode 100644 dwds/debug_extension_mv3/web/copier.dart diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index b24d44629..03f914369 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart @@ -53,6 +53,9 @@ void _registerListeners() { chrome.webNavigation.onCommitted .addListener(allowInterop(_detectNavigationAwayFromDartApp)); + chrome.commands.onCommand + .addListener(allowInterop(_maybeSendCopyAppIdRequest)); + // Detect clicks on the Dart Debug Extension icon. onExtensionIconClicked( allowInterop( @@ -67,7 +70,6 @@ void _registerListeners() { Future _handleRuntimeMessages( dynamic jsRequest, MessageSender sender, - // ignore: avoid-unused-parameters Function sendResponse, ) async { if (jsRequest is! String) return; @@ -155,6 +157,8 @@ Future _handleRuntimeMessages( _setWarningIcon(); }, ); + + sendResponse(defaultResponse); } Future _detectNavigationAwayFromDartApp( @@ -206,6 +210,23 @@ DebugInfo _addTabInfo(DebugInfo debugInfo, {required Tab tab}) { ); } +Future _maybeSendCopyAppIdRequest(String command, [Tab? tab]) async { + if (command != 'copyAppId') return false; + final tabId = (tab ?? await activeTab)?.id; + if (tabId == null) return false; + final debugInfo = await _fetchDebugInfo(tabId); + final workspaceName = debugInfo?.workspaceName; + if (workspaceName == null) return false; + final appId = '$workspaceName-$tabId'; + return sendTabsMessage( + tabId: tabId, + type: MessageType.appId, + body: appId, + sender: Script.background, + recipient: Script.copier, + ); +} + Future _updateIcon(int activeTabId) async { final debugInfo = await _fetchDebugInfo(activeTabId); if (debugInfo == null) { diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart index 937d1d9f7..a7e9db0c9 100644 --- a/dwds/debug_extension_mv3/web/chrome_api.dart +++ b/dwds/debug_extension_mv3/web/chrome_api.dart @@ -12,6 +12,7 @@ external Chrome get chrome; @JS() @anonymous class Chrome { + external Commands get commands; external Debugger get debugger; external Devtools get devtools; external Notifications get notifications; @@ -25,6 +26,20 @@ class Chrome { /// chrome.debugger APIs: /// https://developer.chrome.com/docs/extensions/reference/debugger +@JS() +@anonymous +class Commands { + external OnCommandHandler get onCommand; +} + +@JS() +@anonymous +class OnCommandHandler { + external void addListener( + void Function(String commandName, [Tab? tab]) callback, + ); +} + @JS() @anonymous class Debugger { @@ -228,7 +243,7 @@ class ConnectionHandler { @anonymous class OnMessageHandler { external void addListener( - void Function(dynamic, MessageSender, Function) callback, + dynamic Function(dynamic, MessageSender, Function) callback, ); } @@ -299,6 +314,13 @@ class Tabs { external dynamic remove(int tabId, void Function()? callback); + external Object sendMessage( + int tabId, + Object? message, + Object? options, + void Function() callback, + ); + external OnActivatedHandler get onActivated; external OnRemovedHandler get onRemoved; diff --git a/dwds/debug_extension_mv3/web/copier.dart b/dwds/debug_extension_mv3/web/copier.dart new file mode 100644 index 000000000..79ce3aeaf --- /dev/null +++ b/dwds/debug_extension_mv3/web/copier.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@JS() +library copier; + +import 'dart:html'; + +import 'package:js/js.dart'; + +import 'chrome_api.dart'; +import 'messaging.dart'; + +void main() { + _registerListeners(); +} + +void _registerListeners() { + chrome.runtime.onMessage.addListener( + allowInterop(_handleRuntimeMessages), + ); +} + +void _handleRuntimeMessages( + dynamic jsRequest, + MessageSender sender, + Function sendResponse, +) { + interceptMessage( + message: jsRequest, + expectedType: MessageType.appId, + expectedSender: Script.background, + expectedRecipient: Script.copier, + messageHandler: _copyAppId, + ); + + sendResponse(defaultResponse); +} + +void _copyAppId(String appId) { + final clipboard = window.navigator.clipboard; + if (clipboard == null) return; + clipboard.writeText(appId); + _showCopiedMessage(appId); +} + +Future _showCopiedMessage(String appId) async { + final snackbar = document.createElement('div'); + snackbar.setInnerHtml('Copied app ID: $appId'); + snackbar.classes.addAll(['snackbar', 'snackbar--info', 'show']); + document.body?.append(snackbar); + await Future.delayed(Duration(seconds: 4)); + snackbar.remove(); +} diff --git a/dwds/debug_extension_mv3/web/manifest_mv2.json b/dwds/debug_extension_mv3/web/manifest_mv2.json index 1fba9c10c..9217a67da 100644 --- a/dwds/debug_extension_mv3/web/manifest_mv2.json +++ b/dwds/debug_extension_mv3/web/manifest_mv2.json @@ -24,10 +24,20 @@ "content_scripts": [ { "matches": [""], - "js": ["detector.dart.js"], + "js": ["detector.dart.js", "copier.dart.js"], + "css": ["static_assets/styles.css"], "run_at": "document_end" } ], + "commands": { + "copyAppId": { + "suggestedKey": { + "default": "Ctrl+Shift+7", + "mac": "Command+Shift+7" + }, + "description": "Copy the app ID" + } + }, "web_accessible_resources": ["debug_info.dart.js"], "options_page": "static_assets/settings.html" } diff --git a/dwds/debug_extension_mv3/web/manifest_mv3.json b/dwds/debug_extension_mv3/web/manifest_mv3.json index c0b2960e2..1f92152a4 100644 --- a/dwds/debug_extension_mv3/web/manifest_mv3.json +++ b/dwds/debug_extension_mv3/web/manifest_mv3.json @@ -31,10 +31,20 @@ "content_scripts": [ { "matches": [""], - "js": ["detector.dart.js"], + "js": ["detector.dart.js", "copier.dart.js"], + "css": ["static_assets/styles.css"], "run_at": "document_end" } ], + "commands": { + "copyAppId": { + "suggestedKey": { + "default": "Ctrl+Shift+7", + "mac": "Command+Shift+7" + }, + "description": "Copy the app ID" + } + }, "web_accessible_resources": [ { "matches": [""], diff --git a/dwds/debug_extension_mv3/web/messaging.dart b/dwds/debug_extension_mv3/web/messaging.dart index 95f27deae..f2993014e 100644 --- a/dwds/debug_extension_mv3/web/messaging.dart +++ b/dwds/debug_extension_mv3/web/messaging.dart @@ -7,6 +7,7 @@ library messaging; import 'dart:async'; import 'dart:convert'; +import 'dart:js_util'; import 'package:js/js.dart'; @@ -14,8 +15,15 @@ import 'chrome_api.dart'; import 'data_serializers.dart'; import 'logger.dart'; +// A default response for the sendResponse callback. +// +// Prevents the message port from closing. See: +// https://developer.chrome.com/docs/extensions/mv3/messaging/#simple +final defaultResponse = jsify({'response': 'received'}); + enum Script { background, + copier, debuggerPanel, detector; @@ -27,6 +35,7 @@ enum Script { enum MessageType { isAuthenticated, connectFailure, + appId, debugInfo, debugStateChange, devToolsUrl, @@ -104,34 +113,77 @@ void interceptMessage({ } } +/// Send a message using the chrome.runtime.sendMessage API. Future sendRuntimeMessage({ required MessageType type, required String body, required Script sender, required Script recipient, +}) => + _sendMessage( + type: type, + body: body, + sender: sender, + recipient: recipient, + ); + +/// Send a message using the chrome.tabs.sendMessage API. +Future sendTabsMessage({ + required int tabId, + required MessageType type, + required String body, + required Script sender, + required Script recipient, +}) => + _sendMessage( + tabId: tabId, + type: type, + body: body, + sender: sender, + recipient: recipient, + ); + +Future _sendMessage({ + required MessageType type, + required String body, + required Script sender, + required Script recipient, + int? tabId, }) { final message = Message( to: recipient, from: sender, type: type, body: body, - ); + ).toJSON(); final completer = Completer(); - chrome.runtime.sendMessage( - // id - null, - message.toJSON(), - // options - null, - allowInterop(() { - final error = chrome.runtime.lastError; - if (error != null) { - debugError( - 'Error sending $type to $recipient from $sender: ${error.message}', - ); - } - completer.complete(error != null); - }), - ); + void responseHandler([dynamic _]) { + final error = chrome.runtime.lastError; + if (error != null) { + debugError( + 'Error sending $type to $recipient from $sender: ${error.message}', + ); + } + completer.complete(error != null); + } + + if (tabId != null) { + chrome.tabs.sendMessage( + tabId, + message, + // options + null, + allowInterop(responseHandler), + ); + } else { + chrome.runtime.sendMessage( + // id + null, + message, + // options + null, + allowInterop(responseHandler), + ); + } return completer.future; } diff --git a/dwds/debug_extension_mv3/web/static_assets/styles.css b/dwds/debug_extension_mv3/web/static_assets/styles.css index f5da6a7f6..55a0b4c5e 100644 --- a/dwds/debug_extension_mv3/web/static_assets/styles.css +++ b/dwds/debug_extension_mv3/web/static_assets/styles.css @@ -57,16 +57,16 @@ iframe { } .debugger-card > .mdl-card__title { + background: url("debugger_settings.png"); background-position: center; background-repeat: no-repeat; - background: url("debugger_settings.png"); height: 200px; } .inspector-card > .mdl-card__title { + background: url("inspect_widget.png"); background-position: center; background-repeat: no-repeat; - background: url("inspect_widget.png"); height: 300px; } @@ -75,11 +75,13 @@ h6 { } .snackbar { - border-radius: 2px; bottom: 0px; color: #eeeeee; + font-family: Roboto, 'Helvetica Neue', sans-serif; + left: 0px; padding: 16px; position: fixed; + right: 0px; text-align: center; visibility: hidden; width: 100%; @@ -87,8 +89,8 @@ h6 { } .snackbar > a { - font-weight: bold; color: #eeeeee; + font-weight: bold; } .snackbar--info {