mirror of https://github.com/AMT-Cheif/drift.git
Setup simple devtools extension
This commit is contained in:
parent
2582109614
commit
7b27d21755
|
@ -0,0 +1,4 @@
|
||||||
|
name: drift
|
||||||
|
issue_tracker: https://github.com/simolus3/drift/issues
|
||||||
|
version: 0.0.1
|
||||||
|
material_icon_code_point: '0xf41e'
|
|
@ -61,13 +61,18 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
|
||||||
/// Used by generated code
|
/// Used by generated code
|
||||||
GeneratedDatabase(QueryExecutor executor, {StreamQueryStore? streamStore})
|
GeneratedDatabase(QueryExecutor executor, {StreamQueryStore? streamStore})
|
||||||
: super(executor, streamQueries: streamStore) {
|
: super(executor, streamQueries: streamStore) {
|
||||||
assert(_handleInstantiated());
|
_whenConstructed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used by generated code to connect to a database that is already open.
|
/// Used by generated code to connect to a database that is already open.
|
||||||
GeneratedDatabase.connect(DatabaseConnection connection)
|
GeneratedDatabase.connect(DatabaseConnection connection)
|
||||||
: super.fromConnection(connection) {
|
: super.fromConnection(connection) {
|
||||||
|
_whenConstructed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _whenConstructed() {
|
||||||
assert(_handleInstantiated());
|
assert(_handleInstantiated());
|
||||||
|
devtools.handleCreated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _handleInstantiated() {
|
bool _handleInstantiated() {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import 'package:drift/src/runtime/executor/stream_queries.dart';
|
||||||
import 'package:drift/src/runtime/executor/transactions.dart';
|
import 'package:drift/src/runtime/executor/transactions.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import '../devtools/devtools.dart' as devtools;
|
||||||
|
|
||||||
part 'batch.dart';
|
part 'batch.dart';
|
||||||
part 'connection.dart';
|
part 'connection.dart';
|
||||||
part 'connection_user.dart';
|
part 'connection_user.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<Object?, Object?> 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<TrackedDatabase> all = [];
|
||||||
|
static final Expando<TrackedDatabase> byDatabase = Expando();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCreated(GeneratedDatabase database) {
|
||||||
|
if (_enable) {
|
||||||
|
TrackedDatabase(database);
|
||||||
|
_postChangedEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String describe(GeneratedDatabase database) {
|
||||||
|
return json.encode(DatabaseDescription.fromDrift(database));
|
||||||
|
}
|
|
@ -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<String, Object?>;
|
||||||
|
|
||||||
|
@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<Object>() =>
|
||||||
|
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<ColumnDescription>? 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<EntityDescription> 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);
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'shared.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
TypeDescription _$TypeDescriptionFromJson(Map<String, dynamic> json) =>
|
||||||
|
TypeDescription(
|
||||||
|
type: $enumDecodeNullable(_$DriftSqlTypeEnumMap, json['type']),
|
||||||
|
customTypeName: json['customTypeName'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TypeDescriptionToJson(TypeDescription instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'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<String, dynamic> json) =>
|
||||||
|
ColumnDescription(
|
||||||
|
name: json['name'] as String,
|
||||||
|
type: json['type'] == null
|
||||||
|
? null
|
||||||
|
: TypeDescription.fromJson(json['type'] as Map<String, dynamic>),
|
||||||
|
isNullable: json['isNullable'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ColumnDescriptionToJson(ColumnDescription instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'type': instance.type,
|
||||||
|
'isNullable': instance.isNullable,
|
||||||
|
};
|
||||||
|
|
||||||
|
EntityDescription _$EntityDescriptionFromJson(Map<String, dynamic> json) =>
|
||||||
|
EntityDescription(
|
||||||
|
name: json['name'] as String,
|
||||||
|
type: json['type'] as String,
|
||||||
|
columns: (json['columns'] as List<dynamic>?)
|
||||||
|
?.map((e) => ColumnDescription.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$EntityDescriptionToJson(EntityDescription instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'name': instance.name,
|
||||||
|
'type': instance.type,
|
||||||
|
'columns': instance.columns,
|
||||||
|
};
|
||||||
|
|
||||||
|
DatabaseDescription _$DatabaseDescriptionFromJson(Map<String, dynamic> json) =>
|
||||||
|
DatabaseDescription(
|
||||||
|
dateTimeAsText: json['dateTimeAsText'] as bool,
|
||||||
|
entities: (json['entities'] as List<dynamic>)
|
||||||
|
.map((e) => EntityDescription.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$DatabaseDescriptionToJson(
|
||||||
|
DatabaseDescription instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'dateTimeAsText': instance.dateTimeAsText,
|
||||||
|
'entities': instance.entities,
|
||||||
|
};
|
|
@ -13,6 +13,7 @@ dependencies:
|
||||||
convert: ^3.0.0
|
convert: ^3.0.0
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
js: ^0.6.3
|
js: ^0.6.3
|
||||||
|
json_annotation: ^4.8.1
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
stream_channel: ^2.1.0
|
stream_channel: ^2.1.0
|
||||||
sqlite3: ^2.0.0
|
sqlite3: ^2.0.0
|
||||||
|
@ -24,6 +25,7 @@ dev_dependencies:
|
||||||
build_runner_core: ^7.0.0
|
build_runner_core: ^7.0.0
|
||||||
build_verify: ^3.0.0
|
build_verify: ^3.0.0
|
||||||
build_web_compilers: ^4.0.3
|
build_web_compilers: ^4.0.3
|
||||||
|
json_serializable: ^6.7.1
|
||||||
drift_dev: any
|
drift_dev: any
|
||||||
drift_testcases:
|
drift_testcases:
|
||||||
path: ../extras/integration_tests/drift_testcases
|
path: ../extras/integration_tests/drift_testcases
|
||||||
|
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
||||||
|
```
|
|
@ -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
|
|
@ -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())
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ConsumerStatefulWidget> createState() {
|
||||||
|
return _DatabaseDetailsState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DatabaseDetailsState extends ConsumerState<DatabaseDetails> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<void>((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<List<TrackedDatabase>>((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<InstanceRef>().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?>, TrackedDatabase?>((ref) {
|
||||||
|
final controller = StateController<TrackedDatabase?>(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<ConsumerStatefulWidget> createState() {
|
||||||
|
return _DatabaseListState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DatabaseListState extends ConsumerState<DatabaseList> {
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<RemoteDatabase> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<VmService>.broadcast();
|
||||||
|
void setServiceConnectionForProviderScreen(VmService service) {
|
||||||
|
_serviceConnection.add(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
final serviceProvider = StreamProvider<VmService>((ref) {
|
||||||
|
return _serviceConnection.stream.startWith(serviceManager.service!);
|
||||||
|
});
|
||||||
|
|
||||||
|
final _libraryEvalProvider =
|
||||||
|
FutureProviderFamily<EvalOnDartLibrary, String>((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<ValueNotifier<void>>((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<IsolateRef?>(selectedIsolateListenable.value);
|
||||||
|
|
||||||
|
void listener() => notifier.value = selectedIsolateListenable.value;
|
||||||
|
selectedIsolateListenable.addListener(listener);
|
||||||
|
ref.onDispose(() => selectedIsolateListenable.removeListener(listener));
|
||||||
|
|
||||||
|
return notifier;
|
||||||
|
});
|
|
@ -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
|
Binary file not shown.
After Width: | Height: | Size: 917 B |
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,59 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--
|
||||||
|
If you are serving your web app in a path other than the root, change the
|
||||||
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
|
The path provided below has to start and end with a slash "/" in order for
|
||||||
|
it to work correctly.
|
||||||
|
|
||||||
|
For more details:
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
|
||||||
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
|
the `--base-href` argument provided to `flutter build`.
|
||||||
|
-->
|
||||||
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|
||||||
|
<!-- iOS meta tags & icons -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="drift_devtools_extension">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
|
<title>drift_devtools_extension</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// The value below is injected by flutter build, do not touch.
|
||||||
|
const serviceWorkerVersion = null;
|
||||||
|
</script>
|
||||||
|
<!-- This script adds the flutter initialization JS code -->
|
||||||
|
<script src="flutter.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', function(ev) {
|
||||||
|
// Download main.dart.js
|
||||||
|
_flutter.loader.loadEntrypoint({
|
||||||
|
serviceWorker: {
|
||||||
|
serviceWorkerVersion: serviceWorkerVersion,
|
||||||
|
},
|
||||||
|
onEntrypointLoaded: function(engineInitializer) {
|
||||||
|
engineInitializer.initializeEngine().then(function(appRunner) {
|
||||||
|
appRunner.runApp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ packages:
|
||||||
- sqlparser
|
- sqlparser
|
||||||
- examples/*
|
- examples/*
|
||||||
- extras/benchmarks
|
- extras/benchmarks
|
||||||
|
- extras/drift_devtools_extension
|
||||||
- extras/drift_mariadb
|
- extras/drift_mariadb
|
||||||
- extras/drift_postgres
|
- extras/drift_postgres
|
||||||
- extras/encryption
|
- extras/encryption
|
||||||
|
|
Loading…
Reference in New Issue