example app design update & show active speaker
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:livekit_example/widgets/text_field.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../exts.dart';
|
||||
@@ -105,74 +107,110 @@ class _ConnectPageState extends State<ConnectPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connect to LiveKit'),
|
||||
),
|
||||
body: Center(
|
||||
body: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 20,
|
||||
horizontal: 20,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border:
|
||||
Border.all(color: Theme.of(context).colorScheme.secondary),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 320,
|
||||
),
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: _uriCtrl,
|
||||
decoration: const InputDecoration(labelText: 'URL'),
|
||||
),
|
||||
TextField(
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
controller: _tokenCtrl,
|
||||
decoration: const InputDecoration(labelText: 'Token'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: CheckboxListTile(
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (value) => _setSimulcast(value),
|
||||
title: const Text('Use Simulcast'),
|
||||
value: _simulcast,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: ElevatedButton(
|
||||
onPressed: _busy ? null : () => _connect(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_busy)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 10),
|
||||
child: SizedBox(
|
||||
height: 15,
|
||||
width: 15,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('Connect'),
|
||||
],
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 70),
|
||||
child: SvgPicture.asset(
|
||||
'images/logo-dark.svg',
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: LKTextField(
|
||||
label: 'Server URL',
|
||||
ctrl: _uriCtrl,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 50),
|
||||
child: LKTextField(
|
||||
label: 'Token',
|
||||
ctrl: _tokenCtrl,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
child: ElevatedButton(
|
||||
onPressed: _busy ? null : () => _connect(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_busy)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 10),
|
||||
child: SizedBox(
|
||||
height: 15,
|
||||
width: 15,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('CONNECT'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Container(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// vertical: 20,
|
||||
// horizontal: 20,
|
||||
// ),
|
||||
// decoration: BoxDecoration(
|
||||
// color: Theme.of(context).cardColor,
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// // border: Border.all(
|
||||
// // color: Theme.of(context).colorScheme.secondary),
|
||||
// ),
|
||||
// constraints: const BoxConstraints(
|
||||
// maxWidth: 320,
|
||||
// ),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// TextField(
|
||||
// enableSuggestions: false,
|
||||
// autocorrect: false,
|
||||
// controller: _uriCtrl,
|
||||
// decoration: const InputDecoration(labelText: 'URL'),
|
||||
// ),
|
||||
// TextField(
|
||||
// enableSuggestions: false,
|
||||
// autocorrect: false,
|
||||
// controller: _tokenCtrl,
|
||||
// decoration: const InputDecoration(labelText: 'Token'),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(top: 20),
|
||||
// child: CheckboxListTile(
|
||||
// controlAffinity: ListTileControlAffinity.leading,
|
||||
// onChanged: (value) => _setSimulcast(value),
|
||||
// title: const Text('Use Simulcast'),
|
||||
// value: _simulcast,
|
||||
// ),
|
||||
// ),
|
||||
//
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
]
|
||||
// .map((e) => Padding(
|
||||
// padding: const EdgeInsets.only(bottom: 50), child: e))
|
||||
// .toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -142,10 +142,9 @@ class _RoomPageState extends State<RoomPage> {
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: math.max(0, participants.length - 1),
|
||||
itemBuilder: (BuildContext context, int index) => Container(
|
||||
itemBuilder: (BuildContext context, int index) => SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ParticipantWidget(participants[index + 1],
|
||||
quality: VideoQuality.LOW),
|
||||
),
|
||||
|
||||
+30
-12
@@ -7,17 +7,23 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
// https://github.com/flutter/flutter/issues/55092
|
||||
// https://github.com/flutter/flutter/issues/39113
|
||||
//
|
||||
|
||||
extension LKColors on Colors {
|
||||
static const lkBlue = Color(0xFF5A8BFF);
|
||||
static const lkDarkBlue = Color(0xFF00153C);
|
||||
}
|
||||
|
||||
class LiveKitTheme {
|
||||
//
|
||||
final bgColor = Colors.black;
|
||||
final textColor = Colors.white;
|
||||
final cardColor = const Color(0xFF00163c);
|
||||
final accentColor = const Color(0xFF2d6aef);
|
||||
final cardColor = LKColors.lkDarkBlue;
|
||||
final accentColor = LKColors.lkBlue;
|
||||
|
||||
ThemeData buildThemeData(BuildContext ctx) => ThemeData(
|
||||
backgroundColor: bgColor,
|
||||
// accentColor: accentColor,
|
||||
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
|
||||
// colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: cardColor,
|
||||
),
|
||||
@@ -29,6 +35,14 @@ class LiveKitTheme {
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
textStyle:
|
||||
MaterialStateProperty.all<TextStyle>(GoogleFonts.montserrat(
|
||||
fontSize: 15,
|
||||
)),
|
||||
padding: MaterialStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(vertical: 20, horizontal: 25)),
|
||||
shape: MaterialStateProperty.all<OutlinedBorder>(
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
foregroundColor: MaterialStateProperty.all<Color>(Colors.white),
|
||||
// backgroundColor: MaterialStateProperty.all<Color>(accentColor),
|
||||
backgroundColor: MaterialStateProperty.resolveWith((states) {
|
||||
@@ -43,8 +57,13 @@ class LiveKitTheme {
|
||||
checkColor: MaterialStateProperty.all(Colors.white),
|
||||
fillColor: MaterialStateProperty.all(accentColor),
|
||||
),
|
||||
dialogBackgroundColor: cardColor,
|
||||
textTheme: GoogleFonts.latoTextTheme(
|
||||
dialogTheme: DialogTheme(
|
||||
backgroundColor: cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
textTheme: GoogleFonts.montserratTextTheme(
|
||||
Theme.of(ctx).textTheme,
|
||||
).apply(
|
||||
displayColor: textColor,
|
||||
@@ -53,15 +72,14 @@ class LiveKitTheme {
|
||||
),
|
||||
hintColor: Colors.red,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
labelStyle: TextStyle(
|
||||
color: textColor.withOpacity(.5),
|
||||
labelStyle: const TextStyle(
|
||||
color: LKColors.lkBlue,
|
||||
),
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: textColor.withOpacity(0.1)),
|
||||
),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: accentColor),
|
||||
hintStyle: TextStyle(
|
||||
color: LKColors.lkBlue.withOpacity(.5),
|
||||
),
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,56 +151,74 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
||||
final videoPub = participant.videoTracks.firstOrNull;
|
||||
final videoEnabled = videoPub != null && !videoPub.muted;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _unpublishAll,
|
||||
icon: const Icon(EvaIcons.closeCircleOutline),
|
||||
),
|
||||
if (canMute)
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 15,
|
||||
horizontal: 15,
|
||||
),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 5,
|
||||
runSpacing: 5,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _muteAudio,
|
||||
icon: const Icon(EvaIcons.mic),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _unmuteAudio,
|
||||
icon: const Icon(EvaIcons.micOff),
|
||||
onPressed: _unpublishAll,
|
||||
icon: const Icon(EvaIcons.closeCircleOutline),
|
||||
tooltip: 'Unpublish all',
|
||||
),
|
||||
if (videoEnabled)
|
||||
if (canMute)
|
||||
IconButton(
|
||||
onPressed: _muteAudio,
|
||||
icon: const Icon(EvaIcons.mic),
|
||||
tooltip: 'mute audio',
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _unmuteAudio,
|
||||
icon: const Icon(EvaIcons.micOff),
|
||||
tooltip: 'un-mute audio',
|
||||
),
|
||||
if (videoEnabled)
|
||||
IconButton(
|
||||
onPressed: _muteVideo,
|
||||
icon: const Icon(EvaIcons.video),
|
||||
tooltip: 'mute video',
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _unmuteVideo,
|
||||
icon: const Icon(EvaIcons.videoOff),
|
||||
tooltip: 'un-mute video',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _muteVideo,
|
||||
icon: const Icon(EvaIcons.video),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: _unmuteVideo,
|
||||
icon: const Icon(EvaIcons.videoOff),
|
||||
icon: Icon(position == CameraPosition.back
|
||||
? EvaIcons.camera
|
||||
: EvaIcons.person),
|
||||
onPressed: () => _toggleCamera(),
|
||||
tooltip: 'toggle camera',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(position == CameraPosition.back
|
||||
? EvaIcons.camera
|
||||
: EvaIcons.person),
|
||||
onPressed: () => _toggleCamera(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(EvaIcons.monitor),
|
||||
onPressed: () => _shareScreen(),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onTapDisconnect,
|
||||
icon: const Icon(EvaIcons.closeCircle),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onTapSendData,
|
||||
icon: const Icon(EvaIcons.paperPlane),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onTapReconnect,
|
||||
icon: const Icon(EvaIcons.refresh),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
icon: const Icon(EvaIcons.monitor),
|
||||
onPressed: () => _shareScreen(),
|
||||
tooltip: 'share screen (experimental)',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onTapDisconnect,
|
||||
icon: const Icon(EvaIcons.closeCircle),
|
||||
tooltip: 'disconnect',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onTapSendData,
|
||||
icon: const Icon(EvaIcons.paperPlane),
|
||||
tooltip: 'send demo data',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onTapReconnect,
|
||||
icon: const Icon(EvaIcons.refresh),
|
||||
tooltip: 're-connect',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:livekit_example/theme.dart';
|
||||
|
||||
class NoVideoWidget extends StatelessWidget {
|
||||
//
|
||||
const NoVideoWidget({Key? key}) : super(key: key);
|
||||
@@ -12,7 +14,7 @@ class NoVideoWidget extends StatelessWidget {
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) => Icon(
|
||||
EvaIcons.videoOffOutline,
|
||||
color: Theme.of(ctx).colorScheme.secondary,
|
||||
color: LKColors.lkBlue,
|
||||
size: math.min(constraints.maxHeight, constraints.maxWidth) * 0.3,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:eva_icons_flutter/eva_icons_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:livekit_example/theme.dart';
|
||||
|
||||
import 'no_video.dart';
|
||||
import 'participant_info.dart';
|
||||
@@ -64,7 +65,17 @@ class _ParticipantWidgetState extends State<ParticipantWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) => Container(
|
||||
color: Theme.of(ctx).cardColor,
|
||||
foregroundDecoration: BoxDecoration(
|
||||
border: widget.participant.isSpeaking
|
||||
? Border.all(
|
||||
width: 5,
|
||||
color: LKColors.lkBlue,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(ctx).cardColor,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LKTextField extends StatelessWidget {
|
||||
final String label;
|
||||
final TextEditingController? ctrl;
|
||||
const LKTextField({
|
||||
required this.label,
|
||||
this.ctrl,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 15,
|
||||
horizontal: 15,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.white.withOpacity(.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
decoration: const InputDecoration.collapsed(
|
||||
hintText: '',
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user