From 6c73330f0425320bf35a50a069808afe37401582 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 9 Sep 2024 00:05:12 -0700 Subject: [PATCH] fix: improve landscape keyboard behavior (#1322) * fix: improve landscape keyboard behavior * fix layouting * fix: empty statement * fix format --------- Co-authored-by: SputNikPlop <100245448+SputNikPlop@users.noreply.github.com> --- lib/components/autocomplete.dart | 59 +++++---- lib/components/emote_picker.dart | 9 +- lib/components/message_input.dart | 8 +- lib/screens/home.dart | 198 ++++++++++++++++-------------- pubspec.lock | 4 +- 5 files changed, 157 insertions(+), 121 deletions(-) diff --git a/lib/components/autocomplete.dart b/lib/components/autocomplete.dart index 9aa3b26a..b34986b1 100644 --- a/lib/components/autocomplete.dart +++ b/lib/components/autocomplete.dart @@ -90,35 +90,42 @@ class _AutocompleteWidgetState extends State { if (!snapshot.hasData || lastToken.isEmpty) { return Container(); } - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: (snapshot.data as List) - .where((emote) => emote.code - .toLowerCase() - .startsWith(lastToken.toLowerCase())) - .take(MediaQuery.of(context).size.width ~/ 48) - .map((emote) { - return IconButton( - tooltip: emote.code, - onPressed: () { - widget.controller.text = "${text.substring( - 0, - text.length - lastToken.length, - )}${emote.code} "; - // move cursor position - widget.controller.selection = TextSelection.fromPosition( - TextPosition(offset: widget.controller.text.length)); - }, - splashRadius: 24, - icon: Image( - width: 24, - height: 24, - image: ResilientNetworkImage(emote.uri))); - }).toList(), - ); + return LayoutBuilder(builder: (context, constraints) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: (snapshot.data as List) + .where((emote) => emote.code + .toLowerCase() + .startsWith(lastToken.toLowerCase())) + .take(constraints.maxWidth ~/ 48) + .map((emote) { + return IconButton( + tooltip: emote.code, + onPressed: () { + widget.controller.text = "${text.substring( + 0, + text.length - lastToken.length, + )}${emote.code} "; + // move cursor position + widget.controller.selection = + TextSelection.fromPosition(TextPosition( + offset: widget.controller.text.length)); + }, + splashRadius: 24, + icon: Image( + width: 24, + height: 24, + image: ResilientNetworkImage(emote.uri))); + }).toList(), + ); + }); }, ); case _AutocompleteMode.slashCommand: + if (MediaQuery.of(context).orientation == Orientation.landscape) { + // this is too difficult to show in landscape mode. + return Container(); + } return Container( constraints: const BoxConstraints(maxHeight: 200), child: ListView( diff --git a/lib/components/emote_picker.dart b/lib/components/emote_picker.dart index b16f1894..c9f4f04e 100644 --- a/lib/components/emote_picker.dart +++ b/lib/components/emote_picker.dart @@ -82,8 +82,13 @@ class EmotesList extends StatelessWidget { message: emote.code, preferBelow: false, child: SizedBox( - // Adjust width for 7 emotes per row - width: (MediaQuery.of(context).size.width - 32) / 7 - 8, + // Adjust width for 7 emotes per row in portrait, 10 in landscape. + width: (MediaQuery.of(context).size.width - 32) / + (MediaQuery.of(context).orientation == + Orientation.portrait + ? 7 + : 10) - + 8, height: 36, child: IconButton( onPressed: () => onEmoteSelected(emote), diff --git a/lib/components/message_input.dart b/lib/components/message_input.dart index 42a8e7e6..087cf368 100644 --- a/lib/components/message_input.dart +++ b/lib/components/message_input.dart @@ -122,8 +122,11 @@ class MessageInputWidget extends StatefulWidget { final Channel channel; final List emotes; // TODO: decouple this from the twitch emote model. - const MessageInputWidget( - {super.key, required this.channel, required this.emotes}); + const MessageInputWidget({ + super.key, + required this.channel, + required this.emotes, + }); @override State createState() => _MessageInputWidgetState(); @@ -190,6 +193,7 @@ class _MessageInputWidgetState extends State { @override void dispose() { keyboardSubscription.cancel(); + _chatInputFocusNode.dispose(); _textEditingController.dispose(); super.dispose(); } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 238c2e3b..aea991c7 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,8 +1,10 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:provider/provider.dart'; import 'package:rtchat/audio_channel.dart'; import 'package:rtchat/components/activity_feed_panel.dart'; @@ -164,6 +166,8 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State with TickerProviderStateMixin { final _scaffoldKey = GlobalKey(); + late StreamSubscription keyboardSubscription; + bool _isKeyboardVisible = false; @override void initState() { @@ -186,11 +190,21 @@ class _HomeScreenState extends State with TickerProviderStateMixin { model.showAudioPermissionDialog(context); } }); + + final keyboardVisibilityController = KeyboardVisibilityController(); + // Subscribe to keyboard visibility changes. + keyboardSubscription = + keyboardVisibilityController.onChange.listen((visible) { + setState(() { + _isKeyboardVisible = visible; + }); + }); } @override void dispose() { WakelockPlus.disable(); + keyboardSubscription.cancel(); super.dispose(); } @@ -215,100 +229,106 @@ class _HomeScreenState extends State with TickerProviderStateMixin { FocusManager.instance.primaryFocus?.unfocus(), onEndDrawerChanged: (isOpened) => FocusManager.instance.primaryFocus?.unfocus(), - appBar: HeaderBarWidget( - onChannelSelect: widget.onChannelSelect, - channel: widget.channel, - actions: [ - Consumer2( - builder: (context, activityFeedModel, layoutModel, child) { - if (!activityFeedModel.isEnabled) { - return Container(); - } - return IconButton( - icon: Icon(layoutModel.isShowNotifications - ? Icons.notifications - : Icons.notifications_outlined), - tooltip: AppLocalizations.of(context)!.activityFeed, - onPressed: () { - layoutModel.isShowNotifications = - !layoutModel.isShowNotifications; - }, - ); - }, - ), - if (width > 256) - Consumer( - builder: (context, layoutModel, child) { - return IconButton( - icon: Icon(layoutModel.isShowPreview - ? Icons.preview - : Icons.preview_outlined), - tooltip: AppLocalizations.of(context)!.streamPreview, - onPressed: () { - layoutModel.isShowPreview = - !layoutModel.isShowPreview; + appBar: orientation == Orientation.landscape && _isKeyboardVisible + ? null + : HeaderBarWidget( + onChannelSelect: widget.onChannelSelect, + channel: widget.channel, + actions: [ + Consumer2( + builder: + (context, activityFeedModel, layoutModel, child) { + if (!activityFeedModel.isEnabled) { + return Container(); + } + return IconButton( + icon: Icon(layoutModel.isShowNotifications + ? Icons.notifications + : Icons.notifications_outlined), + tooltip: AppLocalizations.of(context)!.activityFeed, + onPressed: () { + layoutModel.isShowNotifications = + !layoutModel.isShowNotifications; + }, + ); }, - ); - }, - ), - Consumer( - builder: (context, ttsModel, child) { - return IconButton( - icon: Icon( - !kDebugMode - ? (ttsModel.enabled - ? Icons.record_voice_over - : Icons.voice_over_off) - : (ttsModel.newTtsEnabled - ? Icons.record_voice_over - : Icons.voice_over_off), ), - tooltip: AppLocalizations.of(context)!.textToSpeech, - onPressed: () async { - if (!kDebugMode) { - ttsModel.setEnabled(AppLocalizations.of(context)!, - ttsModel.enabled ? false : true); - // Toggle newTtsEnabled and notify listeners immediately - } else { - ttsModel.newTtsEnabled = !ttsModel.newTtsEnabled; + if (width > 256) + Consumer( + builder: (context, layoutModel, child) { + return IconButton( + icon: Icon(layoutModel.isShowPreview + ? Icons.preview + : Icons.preview_outlined), + tooltip: + AppLocalizations.of(context)!.streamPreview, + onPressed: () { + layoutModel.isShowPreview = + !layoutModel.isShowPreview; + }, + ); + }, + ), + Consumer( + builder: (context, ttsModel, child) { + return IconButton( + icon: Icon( + !kDebugMode + ? (ttsModel.enabled + ? Icons.record_voice_over + : Icons.voice_over_off) + : (ttsModel.newTtsEnabled + ? Icons.record_voice_over + : Icons.voice_over_off), + ), + tooltip: AppLocalizations.of(context)!.textToSpeech, + onPressed: () async { + if (!kDebugMode) { + ttsModel.setEnabled( + AppLocalizations.of(context)!, + ttsModel.enabled ? false : true); + // Toggle newTtsEnabled and notify listeners immediately + } else { + ttsModel.newTtsEnabled = + !ttsModel.newTtsEnabled; - if (!ttsModel.newTtsEnabled) { - updateChannelSubscription(""); - await TextToSpeechPlugin.speak( - "Text to speech disabled"); - await TextToSpeechPlugin.disableTTS(); - NotificationsPlugin.cancelNotification(); - } else { - // Start listening to the stream before toggling newTtsEnabled - channelStreamController.stream - .listen((currentChannel) { - if (currentChannel.isEmpty) { - ttsModel.newTtsEnabled = false; + if (!ttsModel.newTtsEnabled) { + updateChannelSubscription(""); + await TextToSpeechPlugin.speak( + "Text to speech disabled"); + await TextToSpeechPlugin.disableTTS(); + NotificationsPlugin.cancelNotification(); + } else { + // Start listening to the stream before toggling newTtsEnabled + channelStreamController.stream + .listen((currentChannel) { + if (currentChannel.isEmpty) { + ttsModel.newTtsEnabled = false; + } + }); + await TextToSpeechPlugin.speak( + "Text to speech enabled"); + updateChannelSubscription( + "${userModel.activeChannel?.provider}:${userModel.activeChannel?.channelId}", + ); + NotificationsPlugin.showNotification(); + NotificationsPlugin.listenToTts(ttsModel); + } } - }); - await TextToSpeechPlugin.speak( - "Text to speech enabled"); - updateChannelSubscription( - "${userModel.activeChannel?.provider}:${userModel.activeChannel?.channelId}", - ); - NotificationsPlugin.showNotification(); - NotificationsPlugin.listenToTts(ttsModel); - } - } - }, - ); - }, - ), - if (userModel.isSignedIn() && width > 256) - IconButton( - icon: const Icon(Icons.people), - tooltip: AppLocalizations.of(context)!.currentViewers, - onPressed: () { - _scaffoldKey.currentState?.openEndDrawer(); - }, + }, + ); + }, + ), + if (userModel.isSignedIn() && width > 256) + IconButton( + icon: const Icon(Icons.people), + tooltip: AppLocalizations.of(context)!.currentViewers, + onPressed: () { + _scaffoldKey.currentState?.openEndDrawer(); + }, + ), + ], ), - ], - ), body: Container( height: mediaQuery.size.height, color: Theme.of(context).scaffoldBackgroundColor, diff --git a/pubspec.lock b/pubspec.lock index dee8846b..00d98488 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1063,10 +1063,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: