Skip to content

Commit

Permalink
fix: improve landscape keyboard behavior (#1322)
Browse files Browse the repository at this point in the history
* fix: improve landscape keyboard behavior

* fix layouting

* fix: empty statement

* fix format

---------

Co-authored-by: SputNikPlop <[email protected]>
  • Loading branch information
kevmo314 and SputNikPlop authored Sep 9, 2024
1 parent 75b45f3 commit 6c73330
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 121 deletions.
59 changes: 33 additions & 26 deletions lib/components/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,35 +90,42 @@ class _AutocompleteWidgetState extends State<AutocompleteWidget> {
if (!snapshot.hasData || lastToken.isEmpty) {
return Container();
}
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: (snapshot.data as List<Emote>)
.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<Emote>)
.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(
Expand Down
9 changes: 7 additions & 2 deletions lib/components/emote_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 6 additions & 2 deletions lib/components/message_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,11 @@ class MessageInputWidget extends StatefulWidget {
final Channel channel;
final List<Emote> 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<MessageInputWidget> createState() => _MessageInputWidgetState();
Expand Down Expand Up @@ -190,6 +193,7 @@ class _MessageInputWidgetState extends State<MessageInputWidget> {
@override
void dispose() {
keyboardSubscription.cancel();
_chatInputFocusNode.dispose();
_textEditingController.dispose();
super.dispose();
}
Expand Down
198 changes: 109 additions & 89 deletions lib/screens/home.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -164,6 +166,8 @@ class HomeScreen extends StatefulWidget {

class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final _scaffoldKey = GlobalKey<ScaffoldState>();
late StreamSubscription<bool> keyboardSubscription;
bool _isKeyboardVisible = false;

@override
void initState() {
Expand All @@ -186,11 +190,21 @@ class _HomeScreenState extends State<HomeScreen> 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();
}

Expand All @@ -215,100 +229,106 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
FocusManager.instance.primaryFocus?.unfocus(),
onEndDrawerChanged: (isOpened) =>
FocusManager.instance.primaryFocus?.unfocus(),
appBar: HeaderBarWidget(
onChannelSelect: widget.onChannelSelect,
channel: widget.channel,
actions: [
Consumer2<ActivityFeedModel, LayoutModel>(
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<LayoutModel>(
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<ActivityFeedModel, LayoutModel>(
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<TtsModel>(
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<LayoutModel>(
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<TtsModel>(
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,
Expand Down
4 changes: 2 additions & 2 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 6c73330

Please sign in to comment.