diff --git a/examples/shop_app_example/devtools_options.yaml b/examples/shop_app_example/devtools_options.yaml index 7e7e7f67d..5c27c3e34 100644 --- a/examples/shop_app_example/devtools_options.yaml +++ b/examples/shop_app_example/devtools_options.yaml @@ -1 +1,2 @@ extensions: + - provider: true \ No newline at end of file diff --git a/packages/talker/example/talker_example.dart b/packages/talker/example/talker_example.dart index b545fd93a..d2299ca31 100644 --- a/packages/talker/example/talker_example.dart +++ b/packages/talker/example/talker_example.dart @@ -2,6 +2,9 @@ import 'package:talker/talker.dart'; Future main() async { final talker = Talker( + logger: TalkerLogger( + output: (message) {}, + ), settings: TalkerSettings( colors: { TalkerKey.info: AnsiPen()..magenta(), @@ -36,7 +39,7 @@ Future main() async { } class YourCustomKey extends TalkerLog { - YourCustomKey(String message) : super(message); + YourCustomKey(String super.message); /// Your own log key (for color customization in settings) static const logKey = 'custom_log_key'; diff --git a/packages/talker_flutter/example/devtools_options.yaml b/packages/talker_flutter/example/devtools_options.yaml new file mode 100644 index 000000000..184e1f328 --- /dev/null +++ b/packages/talker_flutter/example/devtools_options.yaml @@ -0,0 +1,4 @@ +# Enable DevTools extensions for this project. +# Talker tab will appear in DevTools when debugging. +extensions: + - talker_flutter: true diff --git a/packages/talker_flutter/example/macos/Podfile b/packages/talker_flutter/example/macos/Podfile index c795730db..b52666a10 100644 --- a/packages/talker_flutter/example/macos/Podfile +++ b/packages/talker_flutter/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/talker_flutter/example/macos/Podfile.lock b/packages/talker_flutter/example/macos/Podfile.lock index 287709340..45f15855a 100644 --- a/packages/talker_flutter/example/macos/Podfile.lock +++ b/packages/talker_flutter/example/macos/Podfile.lock @@ -20,10 +20,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos SPEC CHECKSUMS: - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/packages/talker_flutter/example/macos/Runner.xcodeproj/project.pbxproj b/packages/talker_flutter/example/macos/Runner.xcodeproj/project.pbxproj index d0086ee81..69b673f7a 100644 --- a/packages/talker_flutter/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/talker_flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -557,7 +557,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -639,7 +639,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -689,7 +689,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/packages/talker_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/talker_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15368eccb..ac78810cd 100644 --- a/packages/talker_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/talker_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -59,6 +59,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/packages/talker_flutter/example/macos/Runner/AppDelegate.swift b/packages/talker_flutter/example/macos/Runner/AppDelegate.swift index 8e02df288..b3c176141 100644 --- a/packages/talker_flutter/example/macos/Runner/AppDelegate.swift +++ b/packages/talker_flutter/example/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/packages/talker_flutter/extension/.gitignore b/packages/talker_flutter/extension/.gitignore new file mode 100644 index 000000000..3820a95c6 --- /dev/null +++ b/packages/talker_flutter/extension/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/talker_flutter/extension/.metadata b/packages/talker_flutter/extension/.metadata new file mode 100644 index 000000000..5722f85d6 --- /dev/null +++ b/packages/talker_flutter/extension/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "9f455d2486bcb28cad87b062475f42edc959f636" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: web + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/talker_flutter/extension/README.md b/packages/talker_flutter/extension/README.md new file mode 100644 index 000000000..6642a5ad2 --- /dev/null +++ b/packages/talker_flutter/extension/README.md @@ -0,0 +1,30 @@ +# Talker DevTools extension + +DevTools extension for [Talker](https://github.com/Frezyx/talker): view app logs in a console inside Flutter DevTools. + +## Build + +From this directory (`packages/talker_flutter/extension`): + +```bash +./build_extension.sh +``` + +Or use the official command (see [DevTools extensions](https://docs.flutter.dev/tools/devtools/extensions)): + +```bash +flutter pub run devtools_extensions build_and_copy --source . --dest devtools +``` + +## Validate + +To check the extension meets DevTools requirements: + +```bash +flutter pub run devtools_extensions validate --package ../ +``` + +## References + +- [Dart & Flutter DevTools Extensions](https://blog.flutter.dev/dart-flutter-devtools-extensions-c8bc1aaf8e5f) +- [DevTools extensions (docs)](https://docs.flutter.dev/tools/devtools/extensions) diff --git a/packages/talker_flutter/extension/analysis_options.yaml b/packages/talker_flutter/extension/analysis_options.yaml new file mode 100644 index 000000000..0d2902135 --- /dev/null +++ b/packages/talker_flutter/extension/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/talker_flutter/extension/build_extension.sh b/packages/talker_flutter/extension/build_extension.sh new file mode 100755 index 000000000..ead505bca --- /dev/null +++ b/packages/talker_flutter/extension/build_extension.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Builds the Talker DevTools extension and copies output to extension/devtools/build. +# Run from packages/talker_flutter/extension (or from repo root: packages/talker_flutter/extension/build_extension.sh). +# +# Alternative (per Flutter DevTools extensions article): +# flutter pub run devtools_extensions build_and_copy --source . --dest devtools + +set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="${SCRIPT_DIR}" +DEST_DIR="${SCRIPT_DIR}/devtools/build" + +echo "[build_extension] Building Flutter web app..." +(cd "$APP_DIR" && flutter build web --release --pwa-strategy=offline-first --no-tree-shake-icons) + +echo "[build_extension] Copying to extension/devtools/build..." +rm -rf "$DEST_DIR" +cp -r "$APP_DIR/build/web" "$DEST_DIR" + +echo "[build_extension] Done. Extension output is in extension/devtools/build/" +echo "[build_extension] Validate: flutter pub run devtools_extensions validate --package ../" diff --git a/packages/talker_flutter/extension/devtools/config.yaml b/packages/talker_flutter/extension/devtools/config.yaml new file mode 100644 index 000000000..939c85ab0 --- /dev/null +++ b/packages/talker_flutter/extension/devtools/config.yaml @@ -0,0 +1,12 @@ +# Flutter DevTools extension configuration for talker_flutter. +# See: https://docs.flutter.dev/tools/devtools/extensions +# Spec: https://blog.flutter.dev/dart-flutter-devtools-extensions-c8bc1aaf8e5f +# Package name this extension belongs to (lowercase, underscores only). Used in title bar. +name: talker_flutter +version: 1.0.0 +# URL for "Report an issue" in DevTools UI [required] +issueTracker: https://github.com/Frezyx/talker/issues +# Material Icons codepoint for extension tab (Icons.list = 0xe896) [required] +materialIconCodePoint: 59542 +# Extension needs connected app to fetch logs [optional, default true] +requiresConnection: true diff --git a/packages/talker_flutter/extension/lib/main.dart b/packages/talker_flutter/extension/lib/main.dart new file mode 100644 index 000000000..d7d8a04cc --- /dev/null +++ b/packages/talker_flutter/extension/lib/main.dart @@ -0,0 +1,19 @@ +import 'package:devtools_extensions/devtools_extensions.dart'; +import 'package:flutter/material.dart'; + +import 'src/talker_logs_screen.dart'; + +void main() { + runApp(const TalkerDevToolsExtension()); +} + +class TalkerDevToolsExtension extends StatelessWidget { + const TalkerDevToolsExtension({super.key}); + + @override + Widget build(BuildContext context) { + return const DevToolsExtension( + child: TalkerLogsScreen(), + ); + } +} diff --git a/packages/talker_flutter/extension/lib/src/talker_logs_screen.dart b/packages/talker_flutter/extension/lib/src/talker_logs_screen.dart new file mode 100644 index 000000000..d10bc4de0 --- /dev/null +++ b/packages/talker_flutter/extension/lib/src/talker_logs_screen.dart @@ -0,0 +1,315 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:devtools_extensions/devtools_extensions.dart'; +import 'package:flutter/material.dart'; + +const String _extensionMethod = 'ext.talker_flutter.getLogs'; +const Duration _pollInterval = Duration(seconds: 1); + +/// Console-style screen that fetches and displays Talker logs from the app +/// via VM Service Extension. +class TalkerLogsScreen extends StatefulWidget { + const TalkerLogsScreen({super.key}); + + @override + State createState() => _TalkerLogsScreenState(); +} + +class _TalkerLogsScreenState extends State { + final List> _logs = []; + int _lastSeenIndex = -1; + Timer? _pollTimer; + String? _error; + bool _loading = true; + + @override + void initState() { + super.initState(); + unawaited(_fetchLogs()); + _pollTimer = Timer.periodic(_pollInterval, (_) => unawaited(_fetchLogs())); + } + + @override + void dispose() { + _pollTimer?.cancel(); + super.dispose(); + } + + Future _fetchLogs() async { + if (!serviceManager.connectedState.value.connected) { + if (mounted) { + setState(() { + _error = 'No app connected. Run your app in debug mode.'; + _loading = false; + }); + } + return; + } + + try { + final isInitialLoad = _lastSeenIndex < 0; + final args = _lastSeenIndex >= 0 ? {'since': '$_lastSeenIndex'} : null; + final response = await serviceManager.callServiceExtensionOnMainIsolate( + _extensionMethod, + args: args, + ); + + var json = response.json; + if (json != null && json.containsKey('result') && json['result'] is String) { + json = jsonDecode(json['result'] as String) as Map?; + } + if (json == null) return; + + final total = json['total'] as int? ?? 0; + final newLogs = json['logs'] as List?; + if (newLogs == null) return; + + if (isInitialLoad) { + _logs.clear(); + for (final entry in newLogs) { + if (entry is Map) _logs.add(entry); + } + _lastSeenIndex = total > 0 ? total - 1 : -1; + } else { + for (final entry in newLogs) { + if (entry is Map) { + final index = entry['index'] as int?; + if (index != null && index > _lastSeenIndex) { + _lastSeenIndex = index; + } + _logs.add(entry); + } + } + } + + if (mounted) { + setState(() { + _error = null; + _loading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _error = 'Failed to fetch logs: $e'; + _loading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Talker Logs'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() { + _logs.clear(); + _lastSeenIndex = -1; + _loading = true; + }); + unawaited(_fetchLogs()); + }, + ), + ], + ), + body: _buildBody(context), + ); + } + + Widget _buildBody(BuildContext context) { + if (_error != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Text( + _error!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ), + ), + ); + } + + if (_loading && _logs.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (_logs.isEmpty) { + return const Center( + child: Text('No logs yet. Use Talker in your app to see logs here.'), + ); + } + + return ListView.builder( + itemCount: _logs.length, + itemBuilder: (context, index) { + return _LogEntryTile(log: _logs[index]); + }, + ); + } +} + +class _LogEntryTile extends StatefulWidget { + const _LogEntryTile({required this.log}); + + final Map log; + + @override + State<_LogEntryTile> createState() => _LogEntryTileState(); +} + +class _LogEntryTileState extends State<_LogEntryTile> { + bool _expanded = false; + + @override + Widget build(BuildContext context) { + final log = widget.log; + final message = log['message'] as String? ?? ''; + final title = log['title'] as String? ?? 'log'; + final logLevel = log['logLevel'] as String? ?? 'debug'; + final time = log['time'] as String? ?? ''; + final stackTrace = log['stackTrace'] as String?; + final exception = log['exception'] as String?; + final error = log['error'] as String?; + final hasDetails = (stackTrace != null && stackTrace.isNotEmpty) || + (exception != null && exception.isNotEmpty) || + (error != null && error.isNotEmpty); + + final color = _colorForLevel(logLevel); + + return Material( + color: _expanded ? Theme.of(context).colorScheme.surfaceContainerHighest : null, + child: InkWell( + onTap: hasDetails + ? () => setState(() => _expanded = !_expanded) + : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 140, + child: Text( + time.length > 19 ? time.substring(11, 19) : time, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + logLevel.toUpperCase(), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + fontFamily: 'monospace', + color: color, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: SelectableText( + '[$title] $message', + style: const TextStyle(fontFamily: 'monospace', fontSize: 13), + ), + ), + if (hasDetails) + Icon( + _expanded ? Icons.expand_less : Icons.expand_more, + size: 20, + ), + ], + ), + if (_expanded && hasDetails) ...[ + const SizedBox(height: 8), + if (exception != null && exception.isNotEmpty) + _DetailBlock(title: 'Exception', content: exception), + if (error != null && error.isNotEmpty) + _DetailBlock(title: 'Error', content: error), + if (stackTrace != null && stackTrace.isNotEmpty) + _DetailBlock(title: 'StackTrace', content: stackTrace), + ], + ], + ), + ), + ), + ); + } + + Color _colorForLevel(String level) { + switch (level.toLowerCase()) { + case 'critical': + case 'error': + return Colors.red; + case 'warning': + return Colors.orange; + case 'info': + return Colors.blue; + case 'debug': + return Colors.grey; + case 'verbose': + return Colors.teal; + default: + return Colors.grey; + } + } +} + +class _DetailBlock extends StatelessWidget { + const _DetailBlock({required this.title, required this.content}); + + final String title; + final String content; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(4), + ), + child: SelectableText( + content, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + ], + ), + ); + } +} diff --git a/packages/talker_flutter/extension/pubspec.lock b/packages/talker_flutter/extension/pubspec.lock new file mode 100644 index 000000000..d7c5954f8 --- /dev/null +++ b/packages/talker_flutter/extension/pubspec.lock @@ -0,0 +1,538 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dap: + dependency: transitive + description: + name: dap + sha256: "42b0b083a09c59a118741769e218fc3738980ab591114f09d1026241d2b9c290" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + dart_service_protocol_shared: + dependency: transitive + description: + name: dart_service_protocol_shared + sha256: "1737875c176d7e3d87bb3a359182828b542fe20a0b34198b8d31a81af5c7a76d" + url: "https://pub.dev" + source: hosted + version: "0.0.3" + dds_service_extensions: + dependency: transitive + description: + name: dds_service_extensions + sha256: afe0fce921953ac0c5bb276bccd7e36fa5035d7769567d122523fdd09beb4d03 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + devtools_app_shared: + dependency: "direct main" + description: + name: devtools_app_shared + sha256: "565f51fcacfe0a44d5fb74679edb5a434307af2ed1aa241a2ee95f38ae8df33d" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + devtools_extensions: + dependency: "direct main" + description: + name: devtools_extensions + sha256: "98b069c5204518f71be04d27ef66107c54daac1148b2232bc0eb48e7a64604ab" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + devtools_shared: + dependency: transitive + description: + name: devtools_shared + sha256: "2daf7a9fba6a470668b26ecbd04200f7bf992aad81a2c31d12457c7791419dea" + url: "https://pub.dev" + source: hosted + version: "12.1.0" + dtd: + dependency: transitive + description: + name: dtd + sha256: "09ddb228b3d1478a093556357692a4c203ff4f9d5f8cda05dfdca0ff3fb7c5d3" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + extension_discovery: + dependency: transitive + description: + name: extension_discovery + sha256: de1fce715ab013cdfb00befc3bdf0914bea5e409c3a567b7f8f144bc061611a7 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + sha256: "82dfd37d3b2e5030ae4729e1d7f5538cbc45eb1c73d618b9272931facac3bec1" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" + url: "https://pub.dev" + source: hosted + version: "0.10.1+2" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: "03c5fa5896080963ab4917eeffda8d28c90f22863a496fb5ba13bc10943e40e4" + url: "https://pub.dev" + source: hosted + version: "0.10.1+1" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" + url: "https://pub.dev" + source: hosted + version: "0.10.3" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sse: + dependency: transitive + description: + name: sse + sha256: fcc97470240bb37377f298e2bd816f09fd7216c07928641c0560719f50603643 + url: "https://pub.dev" + source: hosted + version: "4.1.8" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unified_analytics: + dependency: transitive + description: + name: unified_analytics + sha256: d86b82270d581c183df9930bf6c910f3fabf8b37a0b1817e34094ce40fad818d + url: "https://pub.dev" + source: hosted + version: "8.0.11" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + url: "https://pub.dev" + source: hosted + version: "2.2.4" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/packages/talker_flutter/extension/pubspec.yaml b/packages/talker_flutter/extension/pubspec.yaml new file mode 100644 index 000000000..7c80d8ed2 --- /dev/null +++ b/packages/talker_flutter/extension/pubspec.yaml @@ -0,0 +1,21 @@ +name: talker_devtools_extension +description: DevTools extension for Talker - view app logs in a console inside Flutter DevTools. +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.6.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + devtools_extensions: ^0.5.0 + devtools_app_shared: ^0.5.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + uses-material-design: true diff --git a/packages/talker_flutter/extension/test/widget_test.dart b/packages/talker_flutter/extension/test/widget_test.dart new file mode 100644 index 000000000..0014dfcec --- /dev/null +++ b/packages/talker_flutter/extension/test/widget_test.dart @@ -0,0 +1,10 @@ +// Extension UI is built for web and depends on DevTools globals (serviceManager, etc.) +// which are not available in the VM test environment. For full testing, run in Chrome +// or use the simulated environment (--dart-define=use_simulated_environment=true). +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('placeholder', () { + expect(true, isTrue); + }); +} diff --git a/packages/talker_flutter/extension/web/favicon.png b/packages/talker_flutter/extension/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/packages/talker_flutter/extension/web/favicon.png differ diff --git a/packages/talker_flutter/extension/web/icons/Icon-192.png b/packages/talker_flutter/extension/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/packages/talker_flutter/extension/web/icons/Icon-192.png differ diff --git a/packages/talker_flutter/extension/web/icons/Icon-512.png b/packages/talker_flutter/extension/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/packages/talker_flutter/extension/web/icons/Icon-512.png differ diff --git a/packages/talker_flutter/extension/web/icons/Icon-maskable-192.png b/packages/talker_flutter/extension/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..eb9b4d76e Binary files /dev/null and b/packages/talker_flutter/extension/web/icons/Icon-maskable-192.png differ diff --git a/packages/talker_flutter/extension/web/icons/Icon-maskable-512.png b/packages/talker_flutter/extension/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..d69c56691 Binary files /dev/null and b/packages/talker_flutter/extension/web/icons/Icon-maskable-512.png differ diff --git a/packages/talker_flutter/extension/web/index.html b/packages/talker_flutter/extension/web/index.html new file mode 100644 index 000000000..8173da709 --- /dev/null +++ b/packages/talker_flutter/extension/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + talker_devtools_extension + + + + + + diff --git a/packages/talker_flutter/extension/web/manifest.json b/packages/talker_flutter/extension/web/manifest.json new file mode 100644 index 000000000..bf09feb69 --- /dev/null +++ b/packages/talker_flutter/extension/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "talker_devtools_extension", + "short_name": "talker_devtools_extension", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/talker_flutter/lib/src/devtools/talker_devtools_extension.dart b/packages/talker_flutter/lib/src/devtools/talker_devtools_extension.dart new file mode 100644 index 000000000..8702a9016 --- /dev/null +++ b/packages/talker_flutter/lib/src/devtools/talker_devtools_extension.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; +import 'dart:developer' as developer; + +import 'package:flutter/foundation.dart'; +import 'package:talker/talker.dart'; + +/// Registers [Talker] with Flutter DevTools so the DevTools extension +/// can request logs via VM Service Extension. +/// +/// Only has effect in debug mode. Call [register] explicitly if you +/// create [Talker] without [TalkerFlutter.init]. +class TalkerDevTools { + TalkerDevTools._(); + + static Talker? _devToolsTalker; + static bool _extensionRegistered = false; + + /// Register [talker] for the DevTools extension. In [kDebugMode], + /// [TalkerFlutter.init] does this automatically. + static void register(Talker? talker) { + if (!kDebugMode) return; + _devToolsTalker = talker; + _ensureExtensionRegistered(); + } + + static void _ensureExtensionRegistered() { + if (_extensionRegistered) return; + _extensionRegistered = true; + developer.registerExtension( + _extensionMethodName, + _serviceExtensionHandler, + ); + } + + static const String _extensionMethodName = 'ext.talker_flutter.getLogs'; + + static Future _serviceExtensionHandler( + String method, + Map? params, + ) async { + final talker = _devToolsTalker; + if (talker == null) { + return developer.ServiceExtensionResponse.result( + jsonEncode({ + 'logs': >[], + 'total': 0, + }), + ); + } + + int sinceIndex = -1; + if (params != null && params.containsKey('since')) { + sinceIndex = int.tryParse(params['since']!) ?? -1; + } + + final history = talker.history; + final total = history.length; + final logs = >[]; + for (var i = 0; i < history.length; i++) { + if (i > sinceIndex) { + logs.add(_talkerDataToJson(history[i], i)); + } + } + + return developer.ServiceExtensionResponse.result( + jsonEncode({ + 'logs': logs, + 'total': total, + }), + ); + } + + static Map _talkerDataToJson(TalkerData data, int index) { + return { + 'index': index, + 'message': data.message, + 'key': data.key, + 'logLevel': data.logLevel?.name, + 'title': data.title, + 'time': data.time.toUtc().toIso8601String(), + 'stackTrace': data.stackTrace?.toString(), + 'exception': data.exception?.toString(), + 'error': data.error?.toString(), + }; + } +} diff --git a/packages/talker_flutter/lib/src/extensions/talker_flutter.dart b/packages/talker_flutter/lib/src/extensions/talker_flutter.dart index 0a13795e1..c4ed8a6f6 100644 --- a/packages/talker_flutter/lib/src/extensions/talker_flutter.dart +++ b/packages/talker_flutter/lib/src/extensions/talker_flutter.dart @@ -1,7 +1,9 @@ import 'dart:developer' show log; import 'package:flutter/foundation.dart'; -import 'package:talker_flutter/talker_flutter.dart'; +import 'package:talker/talker.dart'; + +import '../devtools/talker_devtools_extension.dart'; extension TalkerFlutter on Talker { static Talker init({ @@ -9,13 +11,18 @@ extension TalkerFlutter on Talker { TalkerObserver? observer, TalkerSettings? settings, TalkerFilter? filter, - }) => - Talker( - logger: logger ?? TalkerLogger(output: _defaultFlutterOutput), - observer: observer, - settings: settings, - filter: filter, - ); + }) { + final talker = Talker( + logger: logger ?? TalkerLogger(output: _defaultFlutterOutput), + observer: observer, + settings: settings, + filter: filter, + ); + if (kDebugMode) { + TalkerDevTools.register(talker); + } + return talker; + } /// Default output function for Flutter: /// - On web, prints to console. diff --git a/packages/talker_flutter/lib/src/src.dart b/packages/talker_flutter/lib/src/src.dart index 76c2d56f0..a2bee49ec 100644 --- a/packages/talker_flutter/lib/src/src.dart +++ b/packages/talker_flutter/lib/src/src.dart @@ -1,3 +1,4 @@ +export 'devtools/talker_devtools_extension.dart'; export 'extensions/extensions.dart'; export 'observers/observers.dart'; export 'ui/builders/builders.dart';