Add usernames next to non-English names

Thanks to /u/DanMizu for reporting!
This commit is contained in:
tommyxchow 2022-01-19 15:12:54 -05:00
parent 2879df55d8
commit 24cdfd699a
7 changed files with 174 additions and 150 deletions

View file

@ -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]');

View file

@ -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(

View file

@ -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',

View file

@ -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<VideoChat> {
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: [

View file

@ -48,77 +48,78 @@ class _SearchState extends State<Search> {
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),
],
);
},
),
),
),
],
);

View file

@ -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(

View file

@ -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),
),
],