refactoring
This commit is contained in:
@@ -1,21 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide View;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/shared_types/view.dart' as t;
|
||||
import 'package:thermion_flutter/src/widgets/src/resize_observer.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart' hide Texture;
|
||||
|
||||
class ThermionTextureWidget extends StatefulWidget {
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final ThermionViewer viewer;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final t.View view;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
@@ -24,7 +19,7 @@ class ThermionTextureWidget extends StatefulWidget {
|
||||
///
|
||||
/// A callback that will be invoked whenever this widget (and the underlying texture is resized).
|
||||
///
|
||||
final Future Function(Size size, t.View view, double pixelRatio)? onResize;
|
||||
final Future Function(Size size, View view, double pixelRatio)? onResize;
|
||||
|
||||
///
|
||||
/// When true, an FPS counter will be displayed at the top right of the widget
|
||||
@@ -34,7 +29,6 @@ class ThermionTextureWidget extends StatefulWidget {
|
||||
const ThermionTextureWidget({
|
||||
super.key,
|
||||
required this.viewer,
|
||||
required this.view,
|
||||
this.initial,
|
||||
this.onResize,
|
||||
this.showFpsCounter = false,
|
||||
@@ -49,7 +43,7 @@ class ThermionTextureWidget extends StatefulWidget {
|
||||
class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
PlatformTextureDescriptor? _texture;
|
||||
|
||||
static final _views = <t.View>[];
|
||||
static final _views = <View>[];
|
||||
|
||||
final _logger = Logger("_ThermionTextureWidgetState");
|
||||
|
||||
@@ -63,7 +57,7 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_views.remove(widget.view);
|
||||
_views.remove(widget.viewer.view);
|
||||
if (_texture != null) {
|
||||
ThermionFlutterPlatform.instance.destroyTextureDescriptor(_texture!);
|
||||
}
|
||||
@@ -73,10 +67,10 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (_views.contains(widget.view)) {
|
||||
if (_views.contains(widget.viewer.view)) {
|
||||
throw Exception("View already embedded in a widget");
|
||||
}
|
||||
_views.add(widget.view);
|
||||
_views.add(widget.viewer.view);
|
||||
|
||||
// Start FPS counter update timer if enabled
|
||||
if (widget.showFpsCounter) {
|
||||
@@ -95,7 +89,6 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await widget.viewer.initialized;
|
||||
|
||||
var dpr = MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
@@ -111,17 +104,17 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
"Target texture dimensions ${width}x${height} (pixel ratio : $dpr)");
|
||||
|
||||
_texture = await ThermionFlutterPlatform.instance
|
||||
.createTextureAndBindToView(widget.view, width, height);
|
||||
.createTextureAndBindToView(widget.viewer.view, width, height);
|
||||
|
||||
_logger.info(
|
||||
"Actual texture dimensions ${_texture!.width}x${_texture!.height} (pixel ratio : $dpr)");
|
||||
|
||||
await widget.view.setViewport(_texture!.width, _texture!.height);
|
||||
await widget.viewer.view.setViewport(_texture!.width, _texture!.height);
|
||||
|
||||
try {
|
||||
await widget.onResize?.call(
|
||||
Size(_texture!.width.toDouble(), _texture!.height.toDouble()),
|
||||
widget.view,
|
||||
widget.viewer.view,
|
||||
dpr);
|
||||
} catch (err, st) {
|
||||
_logger.severe(err);
|
||||
@@ -191,7 +184,7 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
widget.viewer.msPerFrame - _headroomInMs)) {
|
||||
_rendering = true;
|
||||
if (this == _states.first && _texture != null) {
|
||||
await widget.viewer.requestFrame();
|
||||
await FilamentApp.instance!.requestFrame();
|
||||
lastRender = d.inMilliseconds;
|
||||
|
||||
if (widget.showFpsCounter) {
|
||||
@@ -243,16 +236,16 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
"Resizing texture to dimensions ${newWidth}x${newHeight} (pixel ratio : $dpr)");
|
||||
|
||||
_texture = await ThermionFlutterPlatform.instance
|
||||
.resizeTexture(_texture!, widget.view, newWidth, newHeight);
|
||||
.resizeTexture(_texture!, widget.viewer.view, newWidth, newHeight);
|
||||
|
||||
_logger.info(
|
||||
"Resized texture to dimensions ${_texture!.width}x${_texture!.height} (pixel ratio : $dpr)");
|
||||
|
||||
await widget.view.setViewport(_texture!.width, _texture!.height);
|
||||
await widget.viewer.view.setViewport(_texture!.width, _texture!.height);
|
||||
|
||||
await widget.onResize?.call(
|
||||
Size(_texture!.width.toDouble(), _texture!.height.toDouble()),
|
||||
widget.view,
|
||||
widget.viewer.view,
|
||||
dpr);
|
||||
|
||||
if (!mounted) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide View;
|
||||
import 'package:thermion_flutter/src/widgets/src/thermion_texture_widget.dart';
|
||||
import 'package:thermion_flutter/src/widgets/src/thermion_widget_web.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart';
|
||||
import 'package:thermion_flutter_web/thermion_flutter_web_options.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/shared_types/view.dart' as t;
|
||||
|
||||
Future kDefaultResizeCallback(Size size, t.View view, double pixelRatio) async {
|
||||
Future kDefaultResizeCallback(Size size, View view, double pixelRatio) async {
|
||||
var camera = await view.getCamera();
|
||||
var near = await camera.getNear();
|
||||
var far = await camera.getCullingFar();
|
||||
@@ -26,11 +24,6 @@ class ThermionWidget extends StatefulWidget {
|
||||
///
|
||||
final ThermionViewer viewer;
|
||||
|
||||
///
|
||||
/// The [t.View] associated with this widget. If null, the default View will be used.
|
||||
///
|
||||
final t.View? view;
|
||||
|
||||
///
|
||||
/// A callback to invoke whenever this widget and the underlying surface are
|
||||
/// resized. If a callback is not explicitly provided, the default callback
|
||||
@@ -45,7 +38,7 @@ class ThermionWidget extends StatefulWidget {
|
||||
/// If you need to work with Flutter dimensions, divide [size] by
|
||||
/// [pixelRatio].
|
||||
///
|
||||
final Future Function(Size size, t.View view, double pixelRatio)? onResize;
|
||||
final Future Function(Size size, View view, double pixelRatio)? onResize;
|
||||
|
||||
final bool showFpsCounter;
|
||||
|
||||
@@ -59,7 +52,6 @@ class ThermionWidget extends StatefulWidget {
|
||||
{Key? key,
|
||||
this.initial,
|
||||
required this.viewer,
|
||||
this.view,
|
||||
this.showFpsCounter = false,
|
||||
this.onResize = kDefaultResizeCallback})
|
||||
: super(key: key);
|
||||
@@ -69,42 +61,22 @@ class ThermionWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ThermionWidgetState extends State<ThermionWidget> {
|
||||
t.View? view;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initialize();
|
||||
}
|
||||
|
||||
Future initialize() async {
|
||||
if (widget.view != null) {
|
||||
view = widget.view;
|
||||
} else {
|
||||
view = await widget.viewer.getViewAt(0);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (view == null) {
|
||||
return widget.initial ?? Container(color: Colors.red);
|
||||
}
|
||||
|
||||
// Web doesn't support imported textures yet
|
||||
if (kIsWeb) {
|
||||
return ThermionWidgetWeb(
|
||||
viewer: widget.viewer,
|
||||
options: ThermionFlutterPlugin.options as ThermionFlutterWebOptions?);
|
||||
throw Exception();
|
||||
// return ThermionWidgetWeb(
|
||||
// viewer: widget.viewer,
|
||||
// options: ThermionFlutterPlugin.options as ThermionFlutterWebOptions?);
|
||||
}
|
||||
|
||||
return ThermionTextureWidget(
|
||||
key: ObjectKey(view!),
|
||||
key: ObjectKey(widget.viewer),
|
||||
initial: widget.initial,
|
||||
viewer: widget.viewer,
|
||||
view: view!,
|
||||
showFpsCounter:widget.showFpsCounter,
|
||||
view: widget.viewer.view,
|
||||
showFpsCounter: widget.showFpsCounter,
|
||||
onResize: widget.onResize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:thermion_flutter/thermion_flutter.dart' hide Texture;
|
||||
|
||||
enum ViewerManipulatorType { ORBIT, FIRST_PERSON }
|
||||
|
||||
class ViewerOptions {
|
||||
///
|
||||
///
|
||||
///
|
||||
final Widget initial;
|
||||
|
||||
///
|
||||
/// When true, an FPS counter will be displayed at the top right of the widget
|
||||
///
|
||||
final bool showFpsCounter;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final String? assetPath;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final String? skyboxPath;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final String? iblPath;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final LightType? directLightType;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final bool transformToUnitCube;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final bool postProcessing;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final Color? background;
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
final bool destroyAppOnUnload;
|
||||
|
||||
const ViewerOptions(
|
||||
{this.initial =
|
||||
const DecoratedBox(decoration: BoxDecoration(color: Colors.red)),
|
||||
this.showFpsCounter = false,
|
||||
this.transformToUnitCube = true,
|
||||
this.postProcessing = true,
|
||||
this.destroyAppOnUnload = false,
|
||||
this.assetPath,
|
||||
this.skyboxPath,
|
||||
this.iblPath,
|
||||
this.directLightType,
|
||||
this.background});
|
||||
}
|
||||
|
||||
class ViewerWidget extends StatefulWidget {
|
||||
final ViewerOptions options;
|
||||
|
||||
const ViewerWidget({super.key, this.options = const ViewerOptions()});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ViewerWidgetState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewerWidgetState extends State<ViewerWidget> {
|
||||
ThermionViewer? viewer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ThermionFlutterPlugin.createViewer().then((viewer) async {
|
||||
viewer = viewer;
|
||||
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (viewer != null) {
|
||||
_tearDown();
|
||||
}
|
||||
}
|
||||
|
||||
Future _tearDown() async {
|
||||
await viewer!.dispose();
|
||||
if (widget.options.destroyAppOnUnload) {
|
||||
await viewer!.app.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (viewer == null) {
|
||||
return widget.options.initial!;
|
||||
}
|
||||
return ThermionWidget(viewer: viewer!);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart';
|
||||
import 'package:thermion_dart/thermion_dart.dart' as t;
|
||||
import 'package:thermion_dart/src/filament/filament.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/src/thermion_viewer_ffi.dart';
|
||||
import 'package:thermion_dart/src/viewer/src/ffi/src/ffi_filament_app.dart';
|
||||
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_platform_interface.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_texture.dart';
|
||||
import 'package:thermion_flutter_platform_interface/thermion_flutter_window.dart';
|
||||
|
||||
///
|
||||
/// An abstract implementation of [ThermionFlutterPlatform] that uses
|
||||
/// Flutter platform channels to create a rendering context,
|
||||
/// resource loaders, and surface/render target(s).
|
||||
/// An implementation of [ThermionFlutterPlatform] that uses
|
||||
/// a Flutter platform channel to create a native rendering context, resource
|
||||
/// loader and rendering surfaces.
|
||||
///
|
||||
class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
|
||||
final channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterMethodChannelPlatform");
|
||||
|
||||
late final _logger = Logger(this.runtimeType.toString());
|
||||
|
||||
static SwapChain? _swapChain;
|
||||
|
||||
@@ -31,14 +32,17 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
|
||||
ThermionFlutterPlatform.instance = instance!;
|
||||
}
|
||||
|
||||
t.ThermionViewerFFI? viewer;
|
||||
static Future<Uint8List> loadAsset(String path) async {
|
||||
if (path.startsWith("file://")) {
|
||||
return File(path.replaceAll("file://", "")).readAsBytesSync();
|
||||
}
|
||||
if (path.startsWith("asset://")) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
if (viewer != null) {
|
||||
throw Exception(
|
||||
"Only one ThermionViewer can be created at any given time; ensure you have called [dispose] on the previous instance before constructing a new instance.");
|
||||
}
|
||||
|
||||
var resourceLoader = Pointer<Void>.fromAddress(
|
||||
await channel.invokeMethod("getResourceLoaderWrapper"));
|
||||
|
||||
@@ -47,6 +51,7 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
|
||||
}
|
||||
|
||||
var driverPlatform = await channel.invokeMethod("getDriverPlatform");
|
||||
|
||||
var driverPtr = driverPlatform == null
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(driverPlatform);
|
||||
@@ -57,21 +62,53 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
|
||||
? nullptr
|
||||
: Pointer<Void>.fromAddress(sharedContext);
|
||||
|
||||
viewer = ThermionViewerFFI(
|
||||
late Backend backend;
|
||||
if (options?.backend != null) {
|
||||
switch (options!.backend) {
|
||||
case Backend.VULKAN:
|
||||
if (!Platform.isWindows) {
|
||||
throw Exception("Vulkan only supported on Windows");
|
||||
}
|
||||
case Backend.METAL:
|
||||
if (!Platform.isIOS || !Platform.isMacOS) {
|
||||
throw Exception("Metal only supported on iOS/macOS");
|
||||
}
|
||||
case Backend.OPENGL:
|
||||
if (!Platform.isAndroid) {
|
||||
throw Exception("OpenGL only supported on Android");
|
||||
}
|
||||
default:
|
||||
throw Exception("Unsupported backend");
|
||||
}
|
||||
backend = options.backend!;
|
||||
} else {
|
||||
if (Platform.isWindows) {
|
||||
backend = Backend.VULKAN;
|
||||
} else if (Platform.isMacOS || Platform.isIOS) {
|
||||
backend = Backend.METAL;
|
||||
} else if (Platform.isAndroid) {
|
||||
backend = Backend.OPENGL;
|
||||
} else {
|
||||
throw Exception("Unsupported platform");
|
||||
}
|
||||
}
|
||||
final config = FFIFilamentConfig(
|
||||
backend: backend,
|
||||
resourceLoader: resourceLoader,
|
||||
driver: driverPtr,
|
||||
platform: nullptr,
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: options?.uberarchivePath);
|
||||
await viewer!.initialized;
|
||||
|
||||
viewer!.onDispose(() async {
|
||||
_swapChain = null;
|
||||
this.viewer = null;
|
||||
});
|
||||
await FFIFilamentApp.create(config);
|
||||
|
||||
if (_swapChain != null) {
|
||||
throw Exception("Only a single swapchain can be created");
|
||||
}
|
||||
final viewer = ThermionViewerFFI(
|
||||
loadAsset: loadAsset,
|
||||
);
|
||||
|
||||
await viewer.initialized;
|
||||
|
||||
viewer.onDispose(() async {});
|
||||
|
||||
// this implementation renders directly into a texture/render target
|
||||
// for some reason we still need to create a (headless) swapchain, but the
|
||||
@@ -79,7 +116,7 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
|
||||
// TODO - see if we can use `renderStandaloneView` in FilamentViewer to
|
||||
// avoid this
|
||||
if (Platform.isMacOS || Platform.isIOS) {
|
||||
_swapChain = await viewer!.createHeadlessSwapChain(1, 1);
|
||||
_swapChain = await FilamentApp.instance!.createHeadlessSwapChain(1, 1);
|
||||
}
|
||||
|
||||
return viewer!;
|
||||
@@ -96,8 +133,8 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
|
||||
final hardwareId = result[1] as int;
|
||||
var window = result[2] as int?; // usually 0 for nullptr
|
||||
|
||||
return PlatformTextureDescriptor(flutterId, hardwareId, window, width, height);
|
||||
|
||||
return PlatformTextureDescriptor(
|
||||
flutterId, hardwareId, window, width, height);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -109,43 +146,62 @@ class ThermionFlutterMethodChannelPlatform extends ThermionFlutterPlatform {
|
||||
///
|
||||
///
|
||||
Future<PlatformTextureDescriptor?> createTextureAndBindToView(
|
||||
t.View view, int width, int height) async {
|
||||
View view, int width, int height) async {
|
||||
var descriptor = await createTextureDescriptor(width, height);
|
||||
|
||||
|
||||
if (Platform.isWindows) {
|
||||
if (_swapChain != null) {
|
||||
await view.setRenderable(false, _swapChain!);
|
||||
await viewer!.destroySwapChain(_swapChain!);
|
||||
await FilamentApp.instance!.setRenderable(view, false);
|
||||
await FilamentApp.instance!.destroySwapChain(_swapChain!);
|
||||
}
|
||||
|
||||
_swapChain =
|
||||
await viewer!.createHeadlessSwapChain(descriptor.width, descriptor.height);
|
||||
_swapChain = await FilamentApp.instance!
|
||||
.createHeadlessSwapChain(descriptor.width, descriptor.height);
|
||||
} else if (Platform.isAndroid) {
|
||||
if (_swapChain != null) {
|
||||
await view.setRenderable(false, _swapChain!);
|
||||
await viewer!.destroySwapChain(_swapChain!);
|
||||
await FilamentApp.instance!.setRenderable(view, false);
|
||||
await FilamentApp.instance!.destroySwapChain(_swapChain!);
|
||||
}
|
||||
_swapChain = await viewer!.createSwapChain(descriptor.windowHandle!);
|
||||
_swapChain = await FilamentApp.instance!.createSwapChain(descriptor.windowHandle!);
|
||||
} else {
|
||||
var renderTarget = await viewer!.createRenderTarget(
|
||||
descriptor.width, descriptor.height, descriptor.hardwareId);
|
||||
final color = await FilamentApp.instance!.createTexture(
|
||||
descriptor.width, descriptor.height,
|
||||
importedTextureHandle: descriptor.hardwareId,
|
||||
flags: {
|
||||
TextureUsage.TEXTURE_USAGE_BLIT_DST,
|
||||
TextureUsage.TEXTURE_USAGE_COLOR_ATTACHMENT,
|
||||
TextureUsage.TEXTURE_USAGE_SAMPLEABLE
|
||||
});
|
||||
final depth =
|
||||
await FilamentApp.instance!.createTexture(descriptor.width, descriptor.height, flags: {
|
||||
TextureUsage.TEXTURE_USAGE_BLIT_DST,
|
||||
TextureUsage.TEXTURE_USAGE_DEPTH_ATTACHMENT,
|
||||
TextureUsage.TEXTURE_USAGE_SAMPLEABLE
|
||||
});
|
||||
|
||||
await view.setRenderTarget(renderTarget!);
|
||||
var renderTarget = await FilamentApp.instance!.createRenderTarget(
|
||||
descriptor.width, descriptor.height,
|
||||
color: color, depth: depth);
|
||||
|
||||
await view.setRenderTarget(renderTarget);
|
||||
}
|
||||
await view.setRenderable(true, _swapChain!);
|
||||
await FilamentApp.instance!.register(_swapChain!, view);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@override
|
||||
Future markTextureFrameAvailable(PlatformTextureDescriptor texture) async {
|
||||
await channel.invokeMethod("markTextureFrameAvailable", texture.flutterTextureId);
|
||||
await channel.invokeMethod(
|
||||
"markTextureFrameAvailable", texture.flutterTextureId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PlatformTextureDescriptor> resizeTexture(PlatformTextureDescriptor texture,
|
||||
t.View view, int width, int height) async {
|
||||
Future<PlatformTextureDescriptor> resizeTexture(
|
||||
PlatformTextureDescriptor texture,
|
||||
View view,
|
||||
int width,
|
||||
int height) async {
|
||||
var newTexture = await createTextureAndBindToView(view, width, height);
|
||||
if (newTexture == null) {
|
||||
throw Exception();
|
||||
|
||||
@@ -27,6 +27,8 @@ dependencies:
|
||||
thermion_dart: ^0.2.1-dev.20.0
|
||||
logging: ^1.2.0
|
||||
dependency_overrides:
|
||||
thermion_dart:
|
||||
path: ../../thermion_dart
|
||||
thermion_flutter_platform_interface:
|
||||
path: ../thermion_flutter_platform_interface
|
||||
dev_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user