diff --git a/drift/extension/devtools/config.yaml b/drift/extension/devtools/config.yaml new file mode 100644 index 00000000..e80f7d75 --- /dev/null +++ b/drift/extension/devtools/config.yaml @@ -0,0 +1,4 @@ +name: drift +issue_tracker: https://github.com/simolus3/drift/issues +version: 0.0.1 +material_icon_code_point: '0xf41e' diff --git a/drift/lib/src/runtime/api/db_base.dart b/drift/lib/src/runtime/api/db_base.dart index 5d223f02..ffe7fbfa 100644 --- a/drift/lib/src/runtime/api/db_base.dart +++ b/drift/lib/src/runtime/api/db_base.dart @@ -61,13 +61,18 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser /// Used by generated code GeneratedDatabase(QueryExecutor executor, {StreamQueryStore? streamStore}) : super(executor, streamQueries: streamStore) { - assert(_handleInstantiated()); + _whenConstructed(); } /// Used by generated code to connect to a database that is already open. GeneratedDatabase.connect(DatabaseConnection connection) : super.fromConnection(connection) { + _whenConstructed(); + } + + void _whenConstructed() { assert(_handleInstantiated()); + devtools.handleCreated(this); } bool _handleInstantiated() { diff --git a/drift/lib/src/runtime/api/runtime_api.dart b/drift/lib/src/runtime/api/runtime_api.dart index 2cde80cb..891840ea 100644 --- a/drift/lib/src/runtime/api/runtime_api.dart +++ b/drift/lib/src/runtime/api/runtime_api.dart @@ -6,6 +6,8 @@ import 'package:drift/src/runtime/executor/stream_queries.dart'; import 'package:drift/src/runtime/executor/transactions.dart'; import 'package:meta/meta.dart'; +import '../devtools/devtools.dart' as devtools; + part 'batch.dart'; part 'connection.dart'; part 'connection_user.dart'; diff --git a/drift/lib/src/runtime/devtools/devtools.dart b/drift/lib/src/runtime/devtools/devtools.dart new file mode 100644 index 00000000..52c6f6bb --- /dev/null +++ b/drift/lib/src/runtime/devtools/devtools.dart @@ -0,0 +1,50 @@ +// This file must not be moved, as the devtools extension will try to look up +// types in this exact library. +// ignore_for_file: public_member_api_docs +@internal +library; + +import 'dart:convert'; +import 'dart:developer' as developer; +import 'package:meta/meta.dart'; + +import '../api/runtime_api.dart'; +import 'shared.dart'; + +const _releaseMode = bool.fromEnvironment('dart.vm.product'); +const _profileMode = bool.fromEnvironment('dart.vm.profile'); +const _enable = !_releaseMode && !_profileMode; + +void _postEvent(String type, Map data) { + developer.postEvent('drift:$type', data); +} + +void _postChangedEvent() { + _postEvent('database-list-changed', {}); +} + +class TrackedDatabase { + final GeneratedDatabase database; + final int id; + + TrackedDatabase(this.database) : id = _nextId++ { + byDatabase[database] = this; + all.add(this); + } + + static int _nextId = 0; + + static List all = []; + static final Expando byDatabase = Expando(); +} + +void handleCreated(GeneratedDatabase database) { + if (_enable) { + TrackedDatabase(database); + _postChangedEvent(); + } +} + +String describe(GeneratedDatabase database) { + return json.encode(DatabaseDescription.fromDrift(database)); +} diff --git a/drift/lib/src/runtime/devtools/shared.dart b/drift/lib/src/runtime/devtools/shared.dart new file mode 100644 index 00000000..162c5dc0 --- /dev/null +++ b/drift/lib/src/runtime/devtools/shared.dart @@ -0,0 +1,122 @@ +// ignore_for_file: public_member_api_docs +@internal +library; + +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +import '../api/runtime_api.dart'; +import '../query_builder/query_builder.dart'; +import '../types/mapping.dart'; + +part 'shared.g.dart'; + +typedef JsonObject = Map; + +@JsonSerializable() +class TypeDescription { + final DriftSqlType? type; + final String? customTypeName; + + TypeDescription({this.type, this.customTypeName}); + + factory TypeDescription.fromDrift(GenerationContext ctx, BaseSqlType type) { + return switch (type) { + DriftSqlType() => TypeDescription(type: type), + CustomSqlType() => + TypeDescription(customTypeName: type.sqlTypeName(ctx)), + }; + } + + factory TypeDescription.fromJson(JsonObject obj) => + _$TypeDescriptionFromJson(obj); + + JsonObject toJson() => _$TypeDescriptionToJson(this); +} + +@JsonSerializable() +class ColumnDescription { + final String name; + final TypeDescription? type; + final bool isNullable; + + ColumnDescription( + {required this.name, required this.type, required this.isNullable}); + + factory ColumnDescription.fromDrift( + GenerationContext ctx, GeneratedColumn column) { + return ColumnDescription( + name: column.name, + type: TypeDescription.fromDrift(ctx, column.type), + isNullable: column.$nullable, + ); + } + + factory ColumnDescription.fromJson(JsonObject obj) => + _$ColumnDescriptionFromJson(obj); + + JsonObject toJson() => _$ColumnDescriptionToJson(this); +} + +@JsonSerializable() +class EntityDescription { + final String name; + final String type; + final List? columns; + + EntityDescription( + {required this.name, required this.type, required this.columns}); + + factory EntityDescription.fromDrift( + GenerationContext ctx, DatabaseSchemaEntity entity) { + return EntityDescription( + name: entity.entityName, + type: switch (entity) { + TableInfo() => 'table', + ViewInfo() => 'view', + Index() => 'index', + Trigger() => 'trigger', + _ => 'unknown', + }, + columns: switch (entity) { + ResultSetImplementation() => [ + for (final column in entity.$columns) + ColumnDescription.fromDrift(ctx, column), + ], + _ => null, + }, + ); + } + + factory EntityDescription.fromJson(JsonObject obj) => + _$EntityDescriptionFromJson(obj); + + JsonObject toJson() => _$EntityDescriptionToJson(this); +} + +@JsonSerializable() +class DatabaseDescription { + final bool dateTimeAsText; + final List entities; + + DatabaseDescription({required this.dateTimeAsText, required this.entities}); + + factory DatabaseDescription.fromDrift(GeneratedDatabase database) { + final context = GenerationContext.fromDb(database); + + return DatabaseDescription( + dateTimeAsText: database.options + .createTypeMapping(SqlDialect.sqlite) + .storeDateTimesAsText, + entities: [ + for (final entity in database.allSchemaEntities) + EntityDescription.fromDrift(context, entity), + ], + ); + } + + factory DatabaseDescription.fromJson(JsonObject obj) => + _$DatabaseDescriptionFromJson(obj); + + JsonObject toJson() => _$DatabaseDescriptionToJson(this); +} diff --git a/drift/lib/src/runtime/devtools/shared.g.dart b/drift/lib/src/runtime/devtools/shared.g.dart new file mode 100644 index 00000000..f74fcb71 --- /dev/null +++ b/drift/lib/src/runtime/devtools/shared.g.dart @@ -0,0 +1,77 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shared.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TypeDescription _$TypeDescriptionFromJson(Map json) => + TypeDescription( + type: $enumDecodeNullable(_$DriftSqlTypeEnumMap, json['type']), + customTypeName: json['customTypeName'] as String?, + ); + +Map _$TypeDescriptionToJson(TypeDescription instance) => + { + 'type': _$DriftSqlTypeEnumMap[instance.type], + 'customTypeName': instance.customTypeName, + }; + +const _$DriftSqlTypeEnumMap = { + DriftSqlType.bool: 'bool', + DriftSqlType.string: 'string', + DriftSqlType.bigInt: 'bigInt', + DriftSqlType.int: 'int', + DriftSqlType.dateTime: 'dateTime', + DriftSqlType.blob: 'blob', + DriftSqlType.double: 'double', + DriftSqlType.any: 'any', +}; + +ColumnDescription _$ColumnDescriptionFromJson(Map json) => + ColumnDescription( + name: json['name'] as String, + type: json['type'] == null + ? null + : TypeDescription.fromJson(json['type'] as Map), + isNullable: json['isNullable'] as bool, + ); + +Map _$ColumnDescriptionToJson(ColumnDescription instance) => + { + 'name': instance.name, + 'type': instance.type, + 'isNullable': instance.isNullable, + }; + +EntityDescription _$EntityDescriptionFromJson(Map json) => + EntityDescription( + name: json['name'] as String, + type: json['type'] as String, + columns: (json['columns'] as List?) + ?.map((e) => ColumnDescription.fromJson(e as Map)) + .toList(), + ); + +Map _$EntityDescriptionToJson(EntityDescription instance) => + { + 'name': instance.name, + 'type': instance.type, + 'columns': instance.columns, + }; + +DatabaseDescription _$DatabaseDescriptionFromJson(Map json) => + DatabaseDescription( + dateTimeAsText: json['dateTimeAsText'] as bool, + entities: (json['entities'] as List) + .map((e) => EntityDescription.fromJson(e as Map)) + .toList(), + ); + +Map _$DatabaseDescriptionToJson( + DatabaseDescription instance) => + { + 'dateTimeAsText': instance.dateTimeAsText, + 'entities': instance.entities, + }; diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index 5a20dc52..54d7e06b 100644 --- a/drift/pubspec.yaml +++ b/drift/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: convert: ^3.0.0 collection: ^1.15.0 js: ^0.6.3 + json_annotation: ^4.8.1 meta: ^1.3.0 stream_channel: ^2.1.0 sqlite3: ^2.0.0 @@ -24,6 +25,7 @@ dev_dependencies: build_runner_core: ^7.0.0 build_verify: ^3.0.0 build_web_compilers: ^4.0.3 + json_serializable: ^6.7.1 drift_dev: any drift_testcases: path: ../extras/integration_tests/drift_testcases diff --git a/extras/drift_devtools_extension/.gitignore b/extras/drift_devtools_extension/.gitignore new file mode 100644 index 00000000..24476c5d --- /dev/null +++ b/extras/drift_devtools_extension/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +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 +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# 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/extras/drift_devtools_extension/.metadata b/extras/drift_devtools_extension/.metadata new file mode 100644 index 00000000..5ad07d3d --- /dev/null +++ b/extras/drift_devtools_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: "ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a + base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a + - platform: web + create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a + base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a + + # 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/extras/drift_devtools_extension/README.md b/extras/drift_devtools_extension/README.md new file mode 100644 index 00000000..623726ec --- /dev/null +++ b/extras/drift_devtools_extension/README.md @@ -0,0 +1,10 @@ +This is the [DevTools extension](https://pub.dev/packages/devtools_extensions) for drift, +based on the [drift_db_viewer](https://pub.dev/packages/drift_db_viewer) package. + +## Debugging + +To run the extension in standalone mode, run + +``` +flutter run -d chrome --dart-define=use_simulated_environment=true +``` diff --git a/extras/drift_devtools_extension/analysis_options.yaml b/extras/drift_devtools_extension/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/extras/drift_devtools_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/extras/drift_devtools_extension/lib/main.dart b/extras/drift_devtools_extension/lib/main.dart new file mode 100644 index 00000000..7eb918ff --- /dev/null +++ b/extras/drift_devtools_extension/lib/main.dart @@ -0,0 +1,61 @@ +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_extensions/devtools_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'src/details.dart'; +import 'src/list.dart'; + +void main() { + runApp(const ProviderScope(child: DriftDevToolsExtension())); +} + +class DriftDevToolsExtension extends StatelessWidget { + const DriftDevToolsExtension({super.key}); + + @override + Widget build(BuildContext context) { + return const DevToolsExtension(child: DriftDevtoolsBody()); + } +} + +class DriftDevtoolsBody extends ConsumerWidget { + const DriftDevtoolsBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selected = ref.watch(selectedDatabase); + + return Split( + axis: Split.axisFor(context, 0.85), + initialFractions: const [1 / 3, 2 / 3], + children: [ + const RoundedOutlinedBorder( + child: Column( + children: [ + AreaPaneHeader( + roundedTopBorder: false, + includeTopBorder: false, + title: Text('Drift databases'), + ), + Expanded(child: DatabaseList()), + ], + ), + ), + RoundedOutlinedBorder( + clip: true, + child: Column(children: [ + AreaPaneHeader( + roundedTopBorder: false, + includeTopBorder: false, + title: selected != null + ? Text(selected.typeName) + : const Text('No database selected'), + ), + if (selected != null) const Expanded(child: DatabaseDetails()) + ]), + ), + ], + ); + } +} diff --git a/extras/drift_devtools_extension/lib/src/details.dart b/extras/drift_devtools_extension/lib/src/details.dart new file mode 100644 index 00000000..c914e992 --- /dev/null +++ b/extras/drift_devtools_extension/lib/src/details.dart @@ -0,0 +1,71 @@ +import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_extensions/devtools_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:vm_service/vm_service.dart'; + +import 'list.dart'; +import 'remote_database.dart'; +import 'service.dart'; + +final loadedDatabase = AutoDisposeFutureProvider((ref) async { + final selected = ref.watch(selectedDatabase); + final eval = await ref.watch(driftEvalProvider.future); + + final isAlive = Disposable(); + ref.onDispose(isAlive.dispose); + + if (selected?.database case InstanceRef dbRef) { + final db = await eval.safeGetInstance(dbRef, isAlive); + + return await RemoteDatabase.resolve(db, eval, isAlive); + } + + return null; +}); + +class DatabaseDetails extends ConsumerStatefulWidget { + const DatabaseDetails({super.key}); + + @override + ConsumerState createState() { + return _DatabaseDetailsState(); + } +} + +class _DatabaseDetailsState extends ConsumerState { + final ScrollController controller = ScrollController(); + + @override + void dispose() { + super.dispose(); + controller.dispose(); + } + + @override + Widget build(BuildContext context) { + final database = ref.watch(loadedDatabase); + + return database.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Text('unknown error: $err\n$stack'), + data: (database) { + if (database != null) { + return Scrollbar( + controller: controller, + child: ListView( + controller: controller, + children: [ + for (final entity in database.description.entities) + Text('${entity.name}: ${entity.type}'), + ], + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } +} diff --git a/extras/drift_devtools_extension/lib/src/list.dart b/extras/drift_devtools_extension/lib/src/list.dart new file mode 100644 index 00000000..789ae6dc --- /dev/null +++ b/extras/drift_devtools_extension/lib/src/list.dart @@ -0,0 +1,171 @@ +import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rxdart/transformers.dart'; +import 'package:vm_service/vm_service.dart'; +import 'package:path/path.dart' as p; + +import 'service.dart'; + +class TrackedDatabase { + final int id; + final InstanceRef database; + + TrackedDatabase({required this.id, required this.database}); + + String get typeName => database.classRef!.name!; +} + +final _databaseListChanged = AutoDisposeStreamProvider((ref) { + return Stream.fromFuture(ref.watch(serviceProvider.future)) + .switchMap((serviceProvider) { + return serviceProvider.onExtensionEvent.where((event) { + return event.extensionKind == 'drift:database-list-changed'; + }); + }); +}); + +final databaseList = + AutoDisposeFutureProvider>((ref) async { + ref + ..watch(hotRestartEventProvider) + ..watch(_databaseListChanged); + + final isAlive = Disposable(); + ref.onDispose(isAlive.dispose); + + final eval = await ref.watch(driftEvalProvider.future); + final resultsRefList = + await eval.evalInstance('TrackedDatabase.all', isAlive: isAlive); + + return await Future.wait( + resultsRefList.elements!.cast().map((trackedRef) async { + final trackedDatabase = await eval.safeGetInstance(trackedRef, isAlive); + + final idField = trackedDatabase.fields!.firstWhere((f) => f.name == 'id'); + final databaseField = + trackedDatabase.fields!.firstWhere((f) => f.name == 'database'); + + final (id, database) = await ( + eval.safeGetInstance(idField.value, isAlive), + eval.safeGetInstance(databaseField.value, isAlive) + ).wait; + + return TrackedDatabase( + id: int.parse(id.valueAsString!), + database: database, + ); + })); +}); + +final selectedDatabase = AutoDisposeStateNotifierProvider< + StateController, TrackedDatabase?>((ref) { + final controller = StateController(null); + + ref.listen( + databaseList, + (previous, next) { + final databases = next.asData?.value ?? const []; + + if (databases.isEmpty) { + controller.state = null; + } else if (controller.state == null && + databases.every((e) => e.id != controller.state?.id)) { + controller.state = databases.first; + } + }, + fireImmediately: true, + ); + return controller; +}); + +class DatabaseList extends ConsumerStatefulWidget { + const DatabaseList({super.key}); + + @override + ConsumerState createState() { + return _DatabaseListState(); + } +} + +class _DatabaseListState extends ConsumerState { + static const _tilePadding = EdgeInsets.only( + left: defaultSpacing, + right: densePadding, + top: densePadding, + bottom: densePadding, + ); + + final scrollController = ScrollController(); + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final databases = ref.watch(databaseList); + + return databases.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Padding( + padding: _tilePadding, + child: Text('Could not load databases: $err\n$stack'), + ), + data: (databases) { + return Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: ListView( + primary: false, + controller: scrollController, + children: [ + for (final db in databases) _DatabaseEntry(database: db), + ], + ), + ); + }, + ); + } +} + +class _DatabaseEntry extends ConsumerWidget { + final TrackedDatabase database; + + const _DatabaseEntry({super.key, required this.database}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isSelected = ref.watch(selectedDatabase)?.id == database.id; + final colorScheme = Theme.of(context).colorScheme; + + String? fileName; + int? lineNumber; + + if (database.database.classRef?.location case SourceLocation sl) { + final uri = sl.script?.uri; + if (uri != null) { + fileName = p.url.basename(uri); + } + lineNumber = sl.line; + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Container( + color: isSelected ? colorScheme.selectedRowBackgroundColor : null, + padding: _DatabaseListState._tilePadding, + child: ListTile( + title: Text(database.typeName), + subtitle: fileName != null && lineNumber != null + ? Text('$fileName:$lineNumber') + : null, + ), + ), + ); + } +} diff --git a/extras/drift_devtools_extension/lib/src/remote_database.dart b/extras/drift_devtools_extension/lib/src/remote_database.dart new file mode 100644 index 00000000..3e8e76d4 --- /dev/null +++ b/extras/drift_devtools_extension/lib/src/remote_database.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; + +import 'package:devtools_app_shared/service.dart'; +// ignore: invalid_use_of_internal_member, implementation_imports +import 'package:drift/src/runtime/devtools/shared.dart'; +import 'package:vm_service/vm_service.dart'; + +/// Utilities to access a drift database via service extensions. +class RemoteDatabase { + final DatabaseDescription description; + + RemoteDatabase({required this.description}); + + static Future resolve( + Instance database, + EvalOnDartLibrary eval, + Disposable isAlive, + ) async { + final stringVal = await eval.evalInstance( + 'describe(db)', + isAlive: isAlive, + scope: {'db': database.id!}, + ); + final value = await eval.retrieveFullValueAsString(stringVal); + + final description = DatabaseDescription.fromJson(json.decode(value!)); + + return RemoteDatabase(description: description); + } +} diff --git a/extras/drift_devtools_extension/lib/src/service.dart b/extras/drift_devtools_extension/lib/src/service.dart new file mode 100644 index 00000000..b63f07c9 --- /dev/null +++ b/extras/drift_devtools_extension/lib/src/service.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:devtools_app_shared/service.dart'; +import 'package:devtools_extensions/devtools_extensions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rxdart/transformers.dart'; +import 'package:vm_service/vm_service.dart'; + +final _serviceConnection = StreamController.broadcast(); +void setServiceConnectionForProviderScreen(VmService service) { + _serviceConnection.add(service); +} + +final serviceProvider = StreamProvider((ref) { + return _serviceConnection.stream.startWith(serviceManager.service!); +}); + +final _libraryEvalProvider = + FutureProviderFamily((ref, libraryPath) async { + final service = await ref.watch(serviceProvider.future); + + final eval = EvalOnDartLibrary( + libraryPath, + service, + serviceManager: serviceManager, + ); + ref.onDispose(eval.dispose); + return eval; +}); + +final driftEvalProvider = + _libraryEvalProvider('package:drift/src/runtime/devtools/devtools.dart'); + +final hotRestartEventProvider = + ChangeNotifierProvider>((ref) { + final selectedIsolateListenable = + serviceManager.isolateManager.selectedIsolate; + + // Since ChangeNotifierProvider calls `dispose` on the returned ChangeNotifier + // when the provider is destroyed, we can't simply return `selectedIsolateListenable`. + // So we're making a copy of it instead. + final notifier = ValueNotifier(selectedIsolateListenable.value); + + void listener() => notifier.value = selectedIsolateListenable.value; + selectedIsolateListenable.addListener(listener); + ref.onDispose(() => selectedIsolateListenable.removeListener(listener)); + + return notifier; +}); diff --git a/extras/drift_devtools_extension/pubspec.yaml b/extras/drift_devtools_extension/pubspec.yaml new file mode 100644 index 00000000..f1408c83 --- /dev/null +++ b/extras/drift_devtools_extension/pubspec.yaml @@ -0,0 +1,98 @@ +name: drift_devtools_extension +description: A new Flutter project. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.1.0 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + devtools_extensions: ^0.0.8 + devtools_app_shared: ^0.0.5 + db_viewer: ^1.0.3 + rxdart: ^0.27.7 + flutter_riverpod: ^2.4.4 + vm_service: ^11.10.0 + path: ^1.8.3 + drift: ^2.12.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/extras/drift_devtools_extension/web/favicon.png b/extras/drift_devtools_extension/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/extras/drift_devtools_extension/web/favicon.png differ diff --git a/extras/drift_devtools_extension/web/icons/Icon-192.png b/extras/drift_devtools_extension/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/extras/drift_devtools_extension/web/icons/Icon-192.png differ diff --git a/extras/drift_devtools_extension/web/icons/Icon-512.png b/extras/drift_devtools_extension/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/extras/drift_devtools_extension/web/icons/Icon-512.png differ diff --git a/extras/drift_devtools_extension/web/icons/Icon-maskable-192.png b/extras/drift_devtools_extension/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/extras/drift_devtools_extension/web/icons/Icon-maskable-192.png differ diff --git a/extras/drift_devtools_extension/web/icons/Icon-maskable-512.png b/extras/drift_devtools_extension/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/extras/drift_devtools_extension/web/icons/Icon-maskable-512.png differ diff --git a/extras/drift_devtools_extension/web/index.html b/extras/drift_devtools_extension/web/index.html new file mode 100644 index 00000000..1b100c58 --- /dev/null +++ b/extras/drift_devtools_extension/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + drift_devtools_extension + + + + + + + + + + diff --git a/extras/drift_devtools_extension/web/manifest.json b/extras/drift_devtools_extension/web/manifest.json new file mode 100644 index 00000000..e8ba18ac --- /dev/null +++ b/extras/drift_devtools_extension/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "drift_devtools_extension", + "short_name": "drift_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/melos.yaml b/melos.yaml index d93105ef..a4155226 100644 --- a/melos.yaml +++ b/melos.yaml @@ -9,6 +9,7 @@ packages: - sqlparser - examples/* - extras/benchmarks + - extras/drift_devtools_extension - extras/drift_mariadb - extras/drift_postgres - extras/encryption