From 24cdfd699a6abb560c880f499c480ec47f5529ea Mon Sep 17 00:00:00 2001 From: tommyxchow Date: Wed, 19 Jan 2022 15:12:54 -0500 Subject: [PATCH] Add usernames next to non-English names Thanks to /u/DanMizu for reporting! --- lib/constants/constants.dart | 3 + lib/screens/channel/stores/chat_store.dart | 4 +- lib/screens/channel/video/video_overlay.dart | 184 ++++++++++-------- lib/screens/channel/video_chat.dart | 3 +- lib/screens/home/search/search.dart | 123 ++++++------ .../home/search/search_results_channels.dart | 4 +- lib/screens/home/widgets/stream_card.dart | 3 +- 7 files changed, 174 insertions(+), 150 deletions(-) diff --git a/lib/constants/constants.dart b/lib/constants/constants.dart index f9d4ed5a..ba354c9c 100644 --- a/lib/constants/constants.dart +++ b/lib/constants/constants.dart @@ -21,3 +21,6 @@ const zeroWidthEmotes = [ "cvMask", "cvHazmat", ]; + +// Regex for matching strings that contain lower or upper case English characters. +final regexEnglish = RegExp(r'[a-zA-Z]'); diff --git a/lib/screens/channel/stores/chat_store.dart b/lib/screens/channel/stores/chat_store.dart index 0521cdac..cf743069 100644 --- a/lib/screens/channel/stores/chat_store.dart +++ b/lib/screens/channel/stores/chat_store.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:frosty/constants/constants.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; @@ -183,7 +184,8 @@ abstract class _ChatStoreBase with Store { _channel?.sink.add('PONG :tmi.twitch.tv'); return; } else if (message.contains('Welcome, GLHF!')) { - _messages.add(IRCMessage.createNotice(message: "Connected to $displayName's chat!")); + _messages + .add(IRCMessage.createNotice(message: "Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!")); // Fetch the assets used in chat including badges and emotes. assetsStore.assetsFuture( diff --git a/lib/screens/channel/video/video_overlay.dart b/lib/screens/channel/video/video_overlay.dart index 005ed8bc..ce320122 100644 --- a/lib/screens/channel/video/video_overlay.dart +++ b/lib/screens/channel/video/video_overlay.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:frosty/constants/constants.dart'; import 'package:frosty/screens/channel/stores/video_store.dart'; import 'package:frosty/screens/settings/settings.dart'; import 'package:intl/intl.dart'; @@ -15,43 +16,79 @@ class VideoOverlay extends StatelessWidget { Widget build(BuildContext context) { final orientation = MediaQuery.of(context).orientation; + final backButton = Align( + alignment: Alignment.topLeft, + child: IconButton( + tooltip: 'Back', + icon: Icon( + Icons.adaptive.arrow_back, + color: Colors.white, + ), + onPressed: Navigator.of(context).pop, + ), + ); + + final settingsButton = Align( + alignment: Alignment.topRight, + child: IconButton( + tooltip: 'Settings', + icon: const Icon( + Icons.settings, + color: Colors.white, + ), + onPressed: () => showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => SizedBox( + height: MediaQuery.of(context).size.height * 0.8, + child: Settings(settingsStore: videoStore.settingsStore), + ), + ), + ), + ); + + final refreshButton = IconButton( + tooltip: 'Refresh', + icon: const Icon( + Icons.refresh, + color: Colors.white, + ), + onPressed: videoStore.handleRefresh, + ); + + final streamInfo = videoStore.streamInfo; + + if (streamInfo == null) { + return Stack( + children: [ + backButton, + settingsButton, + Align(alignment: Alignment.bottomRight, child: refreshButton), + ], + ); + } + + final streamer = Text( + regexEnglish.hasMatch(streamInfo.userName) ? streamInfo.userName : streamInfo.userName + ' (${streamInfo.userLogin})', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ); + return Observer( builder: (context) => Stack( children: [ - Align( - alignment: Alignment.topLeft, - child: IconButton( - tooltip: 'Back', - icon: Icon( - Icons.adaptive.arrow_back, - color: Colors.white, - ), - onPressed: Navigator.of(context).pop, - ), - ), - Align( - alignment: Alignment.topRight, - child: IconButton( - tooltip: 'Settings', - icon: const Icon( - Icons.settings, - color: Colors.white, - ), - onPressed: () => showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: (context) => SizedBox( - height: MediaQuery.of(context).size.height * 0.8, - child: Settings(settingsStore: videoStore.settingsStore), - ), - ), - ), - ), + backButton, + settingsButton, + // Add a play button when paused for Android + // When an ad is paused on Android there is no way to unpause, so a play button is necessary. if (Platform.isAndroid) Center( child: IconButton( tooltip: videoStore.paused ? 'Play' : 'Pause', - iconSize: 80.0, + iconSize: 50.0, icon: videoStore.paused ? const Icon( Icons.play_arrow, @@ -82,67 +119,44 @@ class VideoOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( - child: videoStore.streamInfo != null - ? GestureDetector( - onTap: videoStore.handleExpand, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: videoStore.settingsStore.expandInfo - ? Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - videoStore.streamInfo!.userName, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 5.0), - Tooltip( - message: videoStore.streamInfo!.title, - preferBelow: false, - padding: const EdgeInsets.all(10.0), - child: Text( - videoStore.streamInfo!.title, - maxLines: orientation == Orientation.portrait ? 1 : 3, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - ), - ), - ), - const SizedBox(height: 5.0), - Text( - '${videoStore.streamInfo?.gameName} for ${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers', - style: const TextStyle( - color: Colors.white, - ), - ), - ], - ) - : Text( - videoStore.streamInfo!.userName, + child: GestureDetector( + onTap: videoStore.handleExpand, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: videoStore.settingsStore.expandInfo + ? Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + streamer, + const SizedBox(height: 5.0), + Tooltip( + message: videoStore.streamInfo!.title, + preferBelow: false, + padding: const EdgeInsets.all(10.0), + child: Text( + videoStore.streamInfo!.title, + maxLines: orientation == Orientation.portrait ? 1 : 3, + overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, ), ), - ), - ) - : const SizedBox(), - ), - IconButton( - tooltip: 'Refresh', - icon: const Icon( - Icons.refresh, - color: Colors.white, + ), + const SizedBox(height: 5.0), + Text( + '${videoStore.streamInfo?.gameName} for ${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers', + style: const TextStyle( + color: Colors.white, + ), + ), + ], + ) + : streamer, + ), ), - onPressed: videoStore.handleRefresh, ), + refreshButton, if (Platform.isIOS && videoStore.settingsStore.pictureInPicture) IconButton( tooltip: 'Picture-in-Picture', diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart index 3605741c..b5a5ec15 100644 --- a/lib/screens/channel/video_chat.dart +++ b/lib/screens/channel/video_chat.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/api/twitch_api.dart'; +import 'package:frosty/constants/constants.dart'; import 'package:frosty/screens/channel/chat/chat.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; import 'package:frosty/screens/channel/stores/video_store.dart'; @@ -42,7 +43,7 @@ class _VideoChatState extends State { final appBar = AppBar( title: Text( - chatStore.displayName, + regexEnglish.hasMatch(chatStore.displayName) ? chatStore.displayName : chatStore.displayName + ' (${chatStore.channelName})', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), actions: [ diff --git a/lib/screens/home/search/search.dart b/lib/screens/home/search/search.dart index b1c5d30f..077e0061 100644 --- a/lib/screens/home/search/search.dart +++ b/lib/screens/home/search/search.dart @@ -48,77 +48,78 @@ class _SearchState extends State { icon: const Icon(Icons.clear), ), ), - onSubmitted: (query) => setState(() => searchStore.handleQuery(query)), + onSubmitted: searchStore.handleQuery, ), ), Expanded( child: GestureDetector( - onTap: () => FocusManager.instance.primaryFocus?.unfocus(), - child: Observer( - builder: (context) { - if (textEditingController.text.isEmpty || searchStore.channelFuture == null || searchStore.categoryFuture == null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (searchStore.searchHistory.isNotEmpty) - const Padding( - padding: EdgeInsets.all(15.0), - child: Text( - 'HISTORY', - style: TextStyle(fontWeight: FontWeight.bold), - ), + onTap: FocusManager.instance.primaryFocus?.unfocus, + child: Observer( + builder: (context) { + if (textEditingController.text.isEmpty || searchStore.channelFuture == null || searchStore.categoryFuture == null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (searchStore.searchHistory.isNotEmpty) + const Padding( + padding: EdgeInsets.all(15.0), + child: Text( + 'HISTORY', + style: TextStyle(fontWeight: FontWeight.bold), ), - Expanded( - child: ListView( - children: searchStore.searchHistory - .mapIndexed( - (index, searchTerm) => ListTile( - leading: const Icon(Icons.history), - title: Text(searchTerm), - trailing: IconButton( - tooltip: 'Remove', - icon: const Icon(Icons.cancel), - onPressed: () => setState(() { - searchStore.searchHistory.removeAt(index); - }), - ), - onTap: () { - setState(() { - textEditingController.text = searchTerm; - searchStore.handleQuery(searchTerm); - }); - }, + ), + Expanded( + child: ListView( + children: searchStore.searchHistory + .mapIndexed( + (index, searchTerm) => ListTile( + leading: const Icon(Icons.history), + title: Text(searchTerm), + trailing: IconButton( + tooltip: 'Remove', + icon: const Icon(Icons.cancel), + onPressed: () => setState(() { + searchStore.searchHistory.removeAt(index); + }), ), - ) - .toList(), - ), - ), - ], - ); - } - return CustomScrollView( - slivers: [ - const SliverToBoxAdapter( - child: SectionHeader( - 'Channels', - padding: headerPadding, + onTap: () { + setState(() { + textEditingController.text = searchTerm; + searchStore.handleQuery(searchTerm); + }); + }, + ), + ) + .toList(), ), ), - SearchResultsChannels( - searchStore: searchStore, - query: textEditingController.text, - ), - const SliverToBoxAdapter( - child: SectionHeader( - 'Categories', - padding: headerPadding, - ), - ), - SearchResultsCategories(searchStore: searchStore), ], ); - }, - )), + } + return CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: SectionHeader( + 'Channels', + padding: headerPadding, + ), + ), + SearchResultsChannels( + searchStore: searchStore, + query: textEditingController.text, + ), + const SliverToBoxAdapter( + child: SectionHeader( + 'Categories', + padding: headerPadding, + ), + ), + SearchResultsCategories(searchStore: searchStore), + ], + ); + }, + ), + ), ), ], ); diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index ed2542fe..7928e93c 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -4,6 +4,7 @@ import 'package:frosty/api/bttv_api.dart'; import 'package:frosty/api/ffz_api.dart'; import 'package:frosty/api/seventv_api.dart'; import 'package:frosty/api/twitch_api.dart'; +import 'package:frosty/constants/constants.dart'; import 'package:frosty/models/channel.dart'; import 'package:frosty/screens/channel/stores/chat_assets_store.dart'; import 'package:frosty/screens/channel/stores/chat_details_store.dart'; @@ -87,7 +88,8 @@ class SearchResultsChannels extends StatelessWidget { delegate: SliverChildListDelegate.fixed([ ...results.map( (channel) => ListTile( - title: Text(channel.displayName), + title: Text( + regexEnglish.hasMatch(channel.displayName) ? channel.displayName : channel.displayName + ' (${channel.broadcasterLogin})'), leading: ProfilePicture(userLogin: channel.broadcasterLogin), trailing: channel.isLive ? ClipRRect( diff --git a/lib/screens/home/widgets/stream_card.dart b/lib/screens/home/widgets/stream_card.dart index b89a8a64..d1ac2e23 100644 --- a/lib/screens/home/widgets/stream_card.dart +++ b/lib/screens/home/widgets/stream_card.dart @@ -4,6 +4,7 @@ import 'package:frosty/api/bttv_api.dart'; import 'package:frosty/api/ffz_api.dart'; import 'package:frosty/api/seventv_api.dart'; import 'package:frosty/api/twitch_api.dart'; +import 'package:frosty/constants/constants.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/channel/stores/chat_assets_store.dart'; @@ -102,7 +103,7 @@ class StreamCard extends StatelessWidget { ), const SizedBox(width: 5.0), Text( - streamInfo.userName, + regexEnglish.hasMatch(streamInfo.userName) ? streamInfo.userName : streamInfo.userName + ' (${streamInfo.userLogin})', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ],