diff --git a/example/viam_example_app/lib/main.dart b/example/viam_example_app/lib/main.dart index 312dfdd19c..451614eb4c 100644 --- a/example/viam_example_app/lib/main.dart +++ b/example/viam_example_app/lib/main.dart @@ -8,6 +8,7 @@ import 'package:viam_example_app/screens/sensor.dart'; import 'package:viam_example_app/screens/servo.dart'; import 'package:viam_example_app/screens/stream.dart'; import 'package:viam_sdk/viam_sdk.dart'; +import 'package:viam_sdk/widgets.dart'; void main() { runApp(const MyApp()); @@ -137,10 +138,11 @@ class _MyHomePageState extends State { } if (rname.subtype == Base.subtype.resourceSubtype && _cameraName != null) { return BaseScreen( - base: Base.fromRobot(_robot, rname.name), resourceName: rname, - camera: Camera.fromRobot(_robot, _cameraName!.name), - streamClient: _getStream(_cameraName!)); + base: Base.fromRobot(_robot, rname.name), + cameras: + _robot.resourceNames.where((e) => e.subtype == Camera.subtype.resourceSubtype).map((e) => Camera.fromRobot(_robot, e.name)), + robot: _robot); } if (rname.subtype == Board.subtype.resourceSubtype) { return BoardScreen(board: Board.fromRobot(_robot, rname.name), resourceName: rname); @@ -198,11 +200,7 @@ class _MyHomePageState extends State { ]) : _loading ? PlatformCircularProgressIndicator() - : PlatformElevatedButton( - onPressed: () { - _login(); - }, - child: const Text('Login')), + : ViamButton(onPressed: _login, text: 'Login', role: ViamButtonRole.inverse, style: ViamButtonStyle.filled) ], ), ), diff --git a/example/viam_example_app/lib/screens/base.dart b/example/viam_example_app/lib/screens/base.dart index 9ff51f1ccb..522311db91 100644 --- a/example/viam_example_app/lib/screens/base.dart +++ b/example/viam_example_app/lib/screens/base.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:viam_sdk/viam_sdk.dart'; +import 'package:viam_sdk/widgets.dart'; class BaseScreen extends StatelessWidget { final Base base; final ResourceName resourceName; - final Camera camera; - final StreamClient streamClient; + final Iterable cameras; + final RobotClient robot; // TODO change BaseScreen to accept camera ResourceName. - const BaseScreen({Key? key, required this.base, required this.resourceName, required this.camera, required this.streamClient}) + const BaseScreen({Key? key, required this.base, required this.resourceName, required this.cameras, required this.robot}) : super(key: key); @override @@ -20,12 +21,10 @@ class BaseScreen extends StatelessWidget { ), iosContentPadding: true, body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - CameraStreamView(camera: camera, streamClient: streamClient), - BaseJoystick(base: base), - ], + child: ViamBaseScreen( + base: base, + cameras: cameras, + robotClient: robot, ), ), ); diff --git a/example/viam_example_app/lib/screens/stream.dart b/example/viam_example_app/lib/screens/stream.dart index 5bff278740..ad6aed9143 100644 --- a/example/viam_example_app/lib/screens/stream.dart +++ b/example/viam_example_app/lib/screens/stream.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:image/image.dart' as img; import 'package:viam_sdk/viam_sdk.dart'; +import 'package:viam_sdk/widgets.dart'; class StreamScreen extends StatefulWidget { final Camera camera; @@ -81,7 +82,7 @@ class _StreamScreenState extends State { style: const TextStyle(fontWeight: FontWeight.w300), ), const SizedBox(height: 16), - CameraStreamView(camera: widget.camera, streamClient: widget.client), + ViamCameraStreamView(camera: widget.camera, streamClient: widget.client), const SizedBox(height: 16), if (_imgLoaded) Image.memory(Uint8List.view(imageBytes!.buffer), scale: 3), const SizedBox(height: 16), diff --git a/lib/src/widgets/widgets.dart b/lib/src/widgets/widgets.dart deleted file mode 100644 index 4a43dc8075..0000000000 --- a/lib/src/widgets/widgets.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_joystick/flutter_joystick.dart'; -import 'package:flutter_webrtc/flutter_webrtc.dart'; -import 'package:logger/logger.dart'; -import 'package:viam_sdk/viam_sdk.dart'; - -class CameraStreamView extends StatefulWidget { - final Camera camera; - final StreamClient streamClient; - - const CameraStreamView({super.key, required this.camera, required this.streamClient}); - - @override - State createState() => _CameraStreamViewState(); -} - -class _CameraStreamViewState extends State { - late RTCVideoRenderer _renderer; - late StreamSubscription _streamSub; - - @override - void initState() { - _startStream(); - super.initState(); - } - - @override - void deactivate() { - super.deactivate(); - _renderer.dispose(); - widget.streamClient.closeStream(); - _streamSub.cancel(); - } - - Future _startStream() async { - _renderer = RTCVideoRenderer(); - await _renderer.initialize(); - final stream = widget.streamClient.getStream(); - _streamSub = stream.listen((event) { - _renderer.srcObject = event; - setState(() {}); - }); - - _streamSub.onError((error, trace) => Logger().e(error)); - } - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxHeight: 300), - child: RTCVideoView(_renderer), - ); - } -} - -class BaseJoystick extends StatefulWidget { - const BaseJoystick({ - super.key, - required this.base, - }); - - final Base base; - - @override - State createState() => _BaseJoystickState(); -} - -class _BaseJoystickState extends State { - num y = 0; - num z = 0; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text('Y: ${y.round()}% Z: ${z.round()}%'), - const SizedBox(height: 16), - Joystick( - listener: (callSetPower), - ) - ], - ); - } - - void callSetPower(StickDragDetails details) { - widget.base.setPower( - Vector3()..y = details.y * -1, - Vector3()..z = details.x * -1, - ); - setState(() { - y = details.y * -100; - z = details.x * -100; - }); - } -} diff --git a/lib/viam_sdk.dart b/lib/viam_sdk.dart index 44701f3086..9ce64de484 100644 --- a/lib/viam_sdk.dart +++ b/lib/viam_sdk.dart @@ -42,6 +42,3 @@ export 'src/rpc/dial.dart' hide AuthenticatedChannel; /// Misc export 'src/viam_sdk.dart'; - -/// Widgets -export 'src/widgets/widgets.dart'; diff --git a/lib/widgets.dart b/lib/widgets.dart new file mode 100644 index 0000000000..1218e130f7 --- /dev/null +++ b/lib/widgets.dart @@ -0,0 +1,4 @@ +export 'widgets/button.dart'; +export 'widgets/camera_stream.dart'; +export 'widgets/joystick.dart'; +export 'widgets/resources/base.dart'; diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart new file mode 100644 index 0000000000..05a94f7189 --- /dev/null +++ b/lib/widgets/button.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +enum ViamButtonRole { + primary, + inverse, + success, + danger, + warning; + + Color get backgroundColor { + final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; + final isDarkMode = brightness == Brightness.dark; + switch (this) { + case ViamButtonRole.primary: + return isDarkMode ? const Color.fromARGB(255, 40, 40, 41) : const Color.fromARGB(255, 240, 240, 240); + case ViamButtonRole.inverse: + return isDarkMode ? const Color.fromARGB(255, 240, 240, 240) : const Color.fromARGB(255, 40, 40, 41); + case ViamButtonRole.success: + return isDarkMode ? const Color.fromARGB(255, 105, 153, 103) : const Color.fromARGB(255, 61, 125, 63); + case ViamButtonRole.danger: + return isDarkMode ? const Color.fromARGB(255, 211, 103, 94) : const Color.fromARGB(255, 190, 53, 54); + case ViamButtonRole.warning: + return isDarkMode ? const Color.fromARGB(255, 250, 185, 82) : const Color.fromARGB(255, 242, 166, 0); + } + } + + Color get foregroundColor { + final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; + final isDarkMode = brightness == Brightness.dark; + switch (this) { + case ViamButtonRole.primary: + return isDarkMode ? const Color.fromARGB(255, 240, 240, 240) : const Color.fromARGB(255, 40, 40, 41); + case ViamButtonRole.inverse: + return isDarkMode ? const Color.fromARGB(255, 40, 40, 41) : const Color.fromARGB(255, 240, 240, 240); + case ViamButtonRole.success: + return const Color.fromARGB(255, 255, 255, 255); + case ViamButtonRole.danger: + return const Color.fromARGB(255, 255, 255, 255); + case ViamButtonRole.warning: + return const Color.fromARGB(255, 255, 255, 255); + } + } + + MaterialColor get materialColor { + switch (this) { + case ViamButtonRole.primary: + return Colors.grey; + case ViamButtonRole.inverse: + return Colors.grey; + case ViamButtonRole.success: + return Colors.green; + case ViamButtonRole.danger: + return Colors.red; + case ViamButtonRole.warning: + return Colors.amber; + } + } + + ButtonStyle get style => + ButtonStyle(backgroundColor: MaterialStatePropertyAll(backgroundColor), foregroundColor: MaterialStatePropertyAll(foregroundColor)); +} + +enum ViamButtonStyle { + filled, + outline, + ghost; +} + +enum ViamButtonVariant { + iconOnly, + iconLeading, + iconTrailing; +} + +class ViamButton extends StatelessWidget { + final String text; + final VoidCallback onPressed; + final Widget? icon; + final ViamButtonRole role; + final ViamButtonStyle style; + final ViamButtonVariant variant; + + const ViamButton( + {required this.onPressed, + required this.text, + super.key, + this.icon, + this.role = ViamButtonRole.primary, + this.style = ViamButtonStyle.filled, + this.variant = ViamButtonVariant.iconLeading}); + + ButtonStyle get _buttonStyle { + const mainStyle = ButtonStyle(splashFactory: NoSplash.splashFactory); + + if (style == ViamButtonStyle.ghost) { + var fgColor = role.backgroundColor; + if (role == ViamButtonRole.primary || role == ViamButtonRole.inverse) { + fgColor = role.foregroundColor; + } + return mainStyle.copyWith( + backgroundColor: const MaterialStatePropertyAll(Color.fromARGB(0, 0, 0, 0)), + foregroundColor: MaterialStatePropertyAll(fgColor), + ); + } + if (style == ViamButtonStyle.outline) { + var alpha = 25; + var fgColor = role.backgroundColor; + var outlineColor = role.backgroundColor; + if (role == ViamButtonRole.primary || role == ViamButtonRole.inverse) { + alpha = 0; + fgColor = role.foregroundColor; + outlineColor = role.foregroundColor; + } + return mainStyle.copyWith( + backgroundColor: MaterialStatePropertyAll(role.backgroundColor.withAlpha(alpha)), + foregroundColor: MaterialStatePropertyAll(fgColor), + side: MaterialStatePropertyAll(BorderSide(color: outlineColor)), + ); + } + return mainStyle.copyWith( + backgroundColor: MaterialStatePropertyAll(role.backgroundColor), + foregroundColor: MaterialStatePropertyAll(role.foregroundColor), + ); + } + + @override + Widget build(BuildContext context) { + Widget child; + if (icon != null) { + if (variant == ViamButtonVariant.iconOnly) { + child = IconButton(onPressed: onPressed, icon: icon!, style: _buttonStyle); + } + if (style == ViamButtonStyle.outline) { + child = OutlinedButton.icon(onPressed: onPressed, icon: icon!, label: Text(text), style: _buttonStyle); + } + child = TextButton.icon(onPressed: onPressed, icon: icon!, label: Text(text), style: _buttonStyle); + } + if (style == ViamButtonStyle.outline) { + child = OutlinedButton(onPressed: onPressed, style: _buttonStyle, child: Text(text)); + } + child = TextButton(onPressed: onPressed, style: _buttonStyle, child: Text(text)); + + return Theme(data: ThemeData(primarySwatch: role.materialColor), child: child); + } +} diff --git a/lib/widgets/camera_stream.dart b/lib/widgets/camera_stream.dart new file mode 100644 index 0000000000..4e2b6ed768 --- /dev/null +++ b/lib/widgets/camera_stream.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:logger/logger.dart'; +import 'package:viam_sdk/viam_sdk.dart'; + +class ViamCameraStreamView extends StatefulWidget { + final Camera camera; + final StreamClient streamClient; + + const ViamCameraStreamView({super.key, required this.camera, required this.streamClient}); + + @override + State createState() => _ViamCameraStreamViewState(); +} + +class _ViamCameraStreamViewState extends State { + late RTCVideoRenderer _renderer; + late StreamSubscription _streamSub; + + @override + void initState() { + _startStream(); + super.initState(); + } + + @override + void deactivate() { + super.deactivate(); + _renderer.dispose(); + widget.streamClient.closeStream(); + _streamSub.cancel(); + } + + Future _startStream() async { + _renderer = RTCVideoRenderer(); + await _renderer.initialize(); + final stream = widget.streamClient.getStream(); + _streamSub = stream.listen((event) { + _renderer.srcObject = event; + setState(() {}); + }); + + _streamSub.onError((error, trace) => Logger().e(error)); + } + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(maxHeight: 300), + child: RTCVideoView(_renderer), + ); + } +} diff --git a/lib/widgets/joystick.dart b/lib/widgets/joystick.dart new file mode 100644 index 0000000000..7d2f368011 --- /dev/null +++ b/lib/widgets/joystick.dart @@ -0,0 +1,44 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_joystick/flutter_joystick.dart'; +import 'package:viam_sdk/viam_sdk.dart'; + +class ViamBaseJoystick extends StatefulWidget { + const ViamBaseJoystick({ + super.key, + required this.base, + }); + + final Base base; + + @override + State createState() => _ViamBaseJoystickState(); +} + +class _ViamBaseJoystickState extends State { + num y = 0; + num z = 0; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Text('Y: ${y.round()}% Z: ${z.round()}%'), + const SizedBox(height: 16), + Joystick( + listener: (callSetPower), + ) + ], + ); + } + + void callSetPower(StickDragDetails details) { + widget.base.setPower( + Vector3()..y = details.y * -1, + Vector3()..z = details.x * -1, + ); + setState(() { + y = details.y * -100; + z = details.x * -100; + }); + } +} diff --git a/lib/widgets/resources/base.dart b/lib/widgets/resources/base.dart new file mode 100644 index 0000000000..355ef4c2e7 --- /dev/null +++ b/lib/widgets/resources/base.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:viam_sdk/viam_sdk.dart'; +import 'package:viam_sdk/widgets.dart'; + +class ViamBaseScreen extends StatefulWidget { + final Base base; + final Iterable cameras; + final RobotClient robotClient; + + const ViamBaseScreen({ + Key? key, + required this.base, + required this.cameras, + required this.robotClient, + }) : super(key: key); + + @override + State createState() => _ViamBaseScreenState(); +} + +class _ViamBaseScreenState extends State { + Camera? camera; + + @override + void initState() { + camera = widget.cameras.firstOrNull; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.cameras.isNotEmpty) + Center( + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Text('Video feed from: '), + if (widget.cameras.length > 1) + DropdownButton( + value: camera, + icon: const Icon(Icons.keyboard_arrow_down), + items: widget.cameras + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name), + )) + .toList(), + onChanged: (value) => camera = value, + ) + else + Text(camera!.name) + ]), + ), + if (camera != null) ViamCameraStreamView(camera: camera!, streamClient: widget.robotClient.getStream(camera!.name)), + ViamBaseJoystick(base: widget.base) + ], + ), + ), + ); + } +}