Overhaul alert and error messages (#90)
- Added new `AlertMessage` widget that displays an alert/error message with an icon. - Added some new error messages for followed, search, chatters, etc. - Changed various error messages to make them all consistent in style
This commit is contained in:
parent
b0d0f43ec0
commit
3a5f883b1f
19 changed files with 353 additions and 238 deletions
|
|
@ -21,7 +21,7 @@ class BTTVApi {
|
|||
|
||||
return emotes.map((emote) => Emote.fromBTTV(emote, EmoteType.bttvGlobal)).toList();
|
||||
} else {
|
||||
return Future.error('Failed to get BTTV global emotes.');
|
||||
return Future.error('Failed to get BTTV global emotes');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ class BTTVApi {
|
|||
|
||||
return emoteToUrl;
|
||||
} else {
|
||||
return Future.error('Failed to get BTTV channel emotes.');
|
||||
return Future.error('Failed to get BTTV channel emotes');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class FFZApi {
|
|||
|
||||
return emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzGlobal)).toList();
|
||||
} else {
|
||||
return Future.error('Failed to get FFZ global emotes.');
|
||||
return Future.error('Failed to get FFZ global emotes');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ class FFZApi {
|
|||
|
||||
return Tuple2(roomInfo, emotes.map((emote) => Emote.fromFFZ(emote, EmoteType.ffzChannel)).toList());
|
||||
} else {
|
||||
return Future.error('Failed to get FFZ room info.');
|
||||
return Future.error('Failed to get FFZ room info');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ class FFZApi {
|
|||
|
||||
return result;
|
||||
} else {
|
||||
return Future.error('Failed to get FFZ badges.');
|
||||
return Future.error('Failed to get FFZ badges');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class SevenTVApi {
|
|||
|
||||
return result;
|
||||
} else {
|
||||
return Future.error('Failed to get 7TV badges.');
|
||||
return Future.error('Failed to get 7TV badges');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class TwitchApi {
|
|||
|
||||
return emotes.map((emote) => Emote.fromTwitch(emote, EmoteType.twitchGlobal)).toList();
|
||||
} else {
|
||||
return Future.error('Failed to get Twitch global emotes.');
|
||||
return Future.error('Failed to get Twitch global emotes');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ class TwitchApi {
|
|||
}
|
||||
}).toList();
|
||||
} else {
|
||||
return Future.error('Failed to get Twitch channel emotes.');
|
||||
return Future.error('Failed to get Twitch channel emotes');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ class TwitchApi {
|
|||
}
|
||||
}).toList();
|
||||
} else {
|
||||
return Future.error('Failed to get Twitch emotes set.');
|
||||
return Future.error('Failed to get Twitch emotes set');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,12 +97,12 @@ class TwitchApi {
|
|||
if (response.statusCode == 200) {
|
||||
final result = <String, Badge>{};
|
||||
final decoded = jsonDecode(response.body)['badge_sets'] as Map;
|
||||
decoded.forEach((id, versions) => (versions['versions'] as Map)
|
||||
.forEach((version, badgeInfo) => result['$id/$version'] = Badge.fromTwitch(BadgeInfoTwitch.fromJson(badgeInfo))));
|
||||
decoded.forEach((id, versions) =>
|
||||
(versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = Badge.fromTwitch(BadgeInfoTwitch.fromJson(badgeInfo))));
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return Future.error('Failed to get Twitch global badges.');
|
||||
return Future.error('Failed to get Twitch global badges');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,12 +114,12 @@ class TwitchApi {
|
|||
if (response.statusCode == 200) {
|
||||
final result = <String, Badge>{};
|
||||
final decoded = jsonDecode(response.body)['badge_sets'] as Map;
|
||||
decoded.forEach((id, versions) => (versions['versions'] as Map)
|
||||
.forEach((version, badgeInfo) => result['$id/$version'] = Badge.fromTwitch(BadgeInfoTwitch.fromJson(badgeInfo))));
|
||||
decoded.forEach((id, versions) =>
|
||||
(versions['versions'] as Map).forEach((version, badgeInfo) => result['$id/$version'] = Badge.fromTwitch(BadgeInfoTwitch.fromJson(badgeInfo))));
|
||||
|
||||
return result;
|
||||
} else {
|
||||
return Future.error('Failed to get Twitch channel badges.');
|
||||
return Future.error('Failed to get Twitch channel badges');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ class TwitchApi {
|
|||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body)['access_token'];
|
||||
} else {
|
||||
return Future.error('Failed to get default token.');
|
||||
return Future.error('Failed to get default token');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,10 +164,8 @@ class TwitchApi {
|
|||
|
||||
final response = await _client.get(url, headers: {'Authorization': 'Bearer $token'});
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('Token validated!');
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('Token invalidated :(');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -194,9 +192,8 @@ class TwitchApi {
|
|||
required Map<String, String> headers,
|
||||
String? cursor,
|
||||
}) async {
|
||||
final url = Uri.parse(cursor == null
|
||||
? 'https://api.twitch.tv/helix/streams/followed?user_id=$id'
|
||||
: 'https://api.twitch.tv/helix/streams/followed?user_id=$id&after=$cursor');
|
||||
final url = Uri.parse(
|
||||
cursor == null ? 'https://api.twitch.tv/helix/streams/followed?user_id=$id' : 'https://api.twitch.tv/helix/streams/followed?user_id=$id&after=$cursor');
|
||||
|
||||
final response = await _client.get(url, headers: headers);
|
||||
final decoded = jsonDecode(response.body);
|
||||
|
|
@ -213,8 +210,8 @@ class TwitchApi {
|
|||
required Map<String, String> headers,
|
||||
String? cursor,
|
||||
}) async {
|
||||
final url = Uri.parse(
|
||||
cursor == null ? 'https://api.twitch.tv/helix/streams?game_id=$gameId' : 'https://api.twitch.tv/helix/streams?game_id=$gameId&after=$cursor');
|
||||
final url =
|
||||
Uri.parse(cursor == null ? 'https://api.twitch.tv/helix/streams?game_id=$gameId' : 'https://api.twitch.tv/helix/streams?game_id=$gameId&after=$cursor');
|
||||
|
||||
final response = await _client.get(url, headers: headers);
|
||||
final decoded = jsonDecode(response.body);
|
||||
|
|
@ -243,7 +240,7 @@ class TwitchApi {
|
|||
return Future.error('$userLogin is offline');
|
||||
}
|
||||
} else {
|
||||
return Future.error('Failed to get stream info.');
|
||||
return Future.error('Failed to get stream info');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +261,7 @@ class TwitchApi {
|
|||
if (userData.isNotEmpty) {
|
||||
return UserTwitch.fromJson(userData.first);
|
||||
} else {
|
||||
return Future.error('User does not exist!');
|
||||
return Future.error('User does not exist');
|
||||
}
|
||||
} else {
|
||||
return Future.error('Failed to get user: ${decoded['message']}');
|
||||
|
|
@ -306,7 +303,7 @@ class TwitchApi {
|
|||
|
||||
return channelData.map((e) => ChannelQuery.fromJson(e)).toList();
|
||||
} else {
|
||||
return Future.error('Failed to get channels.');
|
||||
return Future.error('Failed to get channels');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -358,7 +355,7 @@ class TwitchApi {
|
|||
|
||||
return CategoriesTwitch.fromJson(decoded);
|
||||
} else {
|
||||
return Future.error('Failed to get categories.');
|
||||
return Future.error('Failed to get categories');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +372,7 @@ class TwitchApi {
|
|||
|
||||
return decoded['total'] as int;
|
||||
} else {
|
||||
return Future.error('Failed to get sub count.');
|
||||
return Future.error('Failed to get sub count');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -389,7 +386,7 @@ class TwitchApi {
|
|||
|
||||
return ChatUsers.fromJson(decoded);
|
||||
} else {
|
||||
return Future.error('Failed to get chatters.');
|
||||
return Future.error('Failed to get chatters');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +419,7 @@ class TwitchApi {
|
|||
return [];
|
||||
}
|
||||
} else {
|
||||
return Future.error('User does not exist!');
|
||||
return Future.error('User does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:frosty/core/auth/auth_store.dart';
|
|||
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_details_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
|
|
@ -62,141 +63,158 @@ class _ChattersListState extends State<ChattersList> {
|
|||
|
||||
final chatDetailsStore = widget.chatDetails;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
autocorrect: false,
|
||||
onChanged: (text) => chatDetailsStore.filterText = text,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: 'Filter chatters',
|
||||
contentPadding: const EdgeInsets.all(10.0),
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Clear Filter',
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatDetailsStore.filterText = '';
|
||||
_textController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
if (chatDetailsStore.error != null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const AlertMessage(
|
||||
message: 'Failed to get chatters',
|
||||
icon: Icons.error,
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
const SizedBox(height: 10.0),
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
onPressed: () => chatDetailsStore.updateChatters(widget.userLogin),
|
||||
child: const Text('Try Again'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
await chatDetailsStore.updateChatters(widget.userLogin);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (context) {
|
||||
if (chatDetailsStore.error != null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Failed to get chatters'),
|
||||
const SizedBox(height: 10.0),
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
onPressed: () => chatDetailsStore.updateChatters(widget.userLogin),
|
||||
child: const Text('Try Again'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
if (chatDetailsStore.chatUsers == null) {
|
||||
return const LoadingIndicator(subtitle: 'Getting chatters...');
|
||||
}
|
||||
|
||||
if (chatDetailsStore.chatUsers == null) {
|
||||
return const LoadingIndicator(subtitle: Text('Getting chatters...'));
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'${NumberFormat().format(chatDetailsStore.chatUsers?.chatterCount)} ${chatDetailsStore.chatUsers?.chatterCount == 1 ? 'Chatter' : 'Chatters'}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...chatDetailsStore.filteredUsers.expandIndexed(
|
||||
(index, users) => [
|
||||
if (users.isNotEmpty) ...[
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
headers[index],
|
||||
fontSize: 12.0,
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
child: Text(users[index]),
|
||||
),
|
||||
onLongPress: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
final userInfo = await context
|
||||
.read<TwitchApi>()
|
||||
.getUser(headers: context.read<AuthStore>().headersTwitch, userLogin: users[index]);
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName: userInfo.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
childCount: users.length,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: chatDetailsStore.showJumpButton ? ScrollToTopButton(scrollController: _scrollController) : null,
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
autocorrect: false,
|
||||
onChanged: (text) => chatDetailsStore.filterText = text,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
labelText: 'Filter chatters',
|
||||
contentPadding: const EdgeInsets.all(10.0),
|
||||
suffixIcon: IconButton(
|
||||
tooltip: 'Clear Filter',
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatDetailsStore.filterText = '';
|
||||
_textController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15.0)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
await chatDetailsStore.updateChatters(widget.userLogin);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'${NumberFormat().format(chatDetailsStore.chatUsers?.chatterCount)} ${chatDetailsStore.chatUsers?.chatterCount == 1 ? 'Chatter' : 'Chatters'}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (chatDetailsStore.filteredUsers.expand((element) => element).isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: AlertMessage(
|
||||
message: 'No matching chatters',
|
||||
icon: Icons.person_off,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
...chatDetailsStore.filteredUsers.expandIndexed(
|
||||
(index, users) => [
|
||||
if (users.isNotEmpty) ...[
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
headers[index],
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
child: Text(users[index]),
|
||||
),
|
||||
onLongPress: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
final userInfo = await context
|
||||
.read<TwitchApi>()
|
||||
.getUser(headers: context.read<AuthStore>().headersTwitch, userLogin: users[index]);
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName: userInfo.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
childCount: users.length,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: chatDetailsStore.showJumpButton ? ScrollToTopButton(scrollController: _scrollController) : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/modal.dart';
|
||||
|
|
@ -89,45 +90,48 @@ class _ChatUserModalState extends State<ChatUserModal> {
|
|||
'Recent Messages',
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Expanded(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: widget.chatStore.settings.messageScale),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: widget.chatStore.settings.fontSize),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
reverse: true,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onLongPress: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
if (userMessages.isEmpty)
|
||||
const Expanded(child: AlertMessage(message: 'No recent messages'))
|
||||
else
|
||||
Expanded(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: widget.chatStore.settings.messageScale),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: widget.chatStore.settings.fontSize),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
reverse: true,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onLongPress: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
await Clipboard.setData(ClipboardData(text: userMessages[index].message));
|
||||
await Clipboard.setData(ClipboardData(text: userMessages[index].message));
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Message copied!'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ChatMessage(
|
||||
ircMessage: userMessages[index],
|
||||
assetsStore: widget.chatStore.assetsStore,
|
||||
settingsStore: widget.chatStore.settings,
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: AlertMessage(message: 'Message copied to clipboard'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ChatMessage(
|
||||
ircMessage: userMessages[index],
|
||||
assetsStore: widget.chatStore.assetsStore,
|
||||
settingsStore: widget.chatStore.settings,
|
||||
),
|
||||
),
|
||||
separatorBuilder: (context, index) => widget.chatStore.settings.showChatMessageDividers
|
||||
? Divider(
|
||||
height: widget.chatStore.settings.messageSpacing,
|
||||
thickness: 1.0,
|
||||
)
|
||||
: SizedBox(height: widget.chatStore.settings.messageSpacing),
|
||||
itemCount: userMessages.length,
|
||||
),
|
||||
separatorBuilder: (context, index) => widget.chatStore.settings.showChatMessageDividers
|
||||
? Divider(
|
||||
height: widget.chatStore.settings.messageSpacing,
|
||||
thickness: 1.0,
|
||||
)
|
||||
: SizedBox(height: widget.chatStore.settings.messageSpacing),
|
||||
itemCount: userMessages.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ abstract class ChatDetailsStoreBase with Store {
|
|||
_chatUsers = await twitchApi.getChatters(userLogin: userLogin);
|
||||
_error = null;
|
||||
} on SocketException {
|
||||
_error = 'Failed to connect :(';
|
||||
_error = 'Failed to connect';
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
_error = e.toString();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:frosty/core/auth/auth_store.dart';
|
|||
import 'package:frosty/screens/home/search/search_results_categories.dart';
|
||||
import 'package:frosty/screens/home/search/search_results_channels.dart';
|
||||
import 'package:frosty/screens/home/stores/search_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
|
@ -60,15 +61,21 @@ class _SearchState extends State<Search> {
|
|||
child: Observer(
|
||||
builder: (context) {
|
||||
if (_textEditingController.text.isEmpty || _searchStore.channelFuture == null || _searchStore.categoryFuture == null) {
|
||||
if (_searchStore.searchHistory.isEmpty) {
|
||||
return const AlertMessage(
|
||||
message: 'No recent searches',
|
||||
icon: Icons.search_off,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_searchStore.searchHistory.isNotEmpty)
|
||||
const SectionHeader(
|
||||
'History',
|
||||
fontSize: 12.0,
|
||||
padding: headerPadding,
|
||||
),
|
||||
const SectionHeader(
|
||||
'History',
|
||||
fontSize: 12.0,
|
||||
padding: headerPadding,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: _searchStore.searchHistory
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:frosty/models/category.dart';
|
||||
import 'package:frosty/screens/home/stores/search_store.dart';
|
||||
import 'package:frosty/screens/home/widgets/category_card.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
|
|
@ -20,14 +21,21 @@ class SearchResultsCategories extends StatelessWidget {
|
|||
switch (future!.status) {
|
||||
case FutureStatus.pending:
|
||||
return const SliverToBoxAdapter(
|
||||
child: LoadingIndicator(
|
||||
subtitle: Text('Loading categories...'),
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: LoadingIndicator(
|
||||
subtitle: 'Loading categories...',
|
||||
),
|
||||
),
|
||||
);
|
||||
case FutureStatus.rejected:
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text('Failed to get categories'),
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: AlertMessage(
|
||||
message: 'Failed to get categories',
|
||||
icon: Icons.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
case FutureStatus.fulfilled:
|
||||
|
|
@ -35,8 +43,12 @@ class SearchResultsCategories extends StatelessWidget {
|
|||
|
||||
if (categories == null) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text('Failed to get categories'),
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: AlertMessage(
|
||||
message: 'Failed to get categories',
|
||||
icon: Icons.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -45,7 +57,7 @@ class SearchResultsCategories extends StatelessWidget {
|
|||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: Center(child: Text('No matching categories')),
|
||||
child: AlertMessage(message: 'No matching categories'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:frosty/constants/constants.dart';
|
|||
import 'package:frosty/models/channel.dart';
|
||||
import 'package:frosty/screens/channel/video_chat.dart';
|
||||
import 'package:frosty/screens/home/stores/search_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
|
|
@ -40,11 +41,15 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text(e.toString()),
|
||||
content: AlertMessage(
|
||||
message: error.toString(),
|
||||
icon: Icons.error,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
|
|
@ -59,13 +64,17 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
case FutureStatus.pending:
|
||||
return const SliverToBoxAdapter(
|
||||
child: LoadingIndicator(
|
||||
subtitle: Text('Loading channels...'),
|
||||
subtitle: 'Loading channels...',
|
||||
),
|
||||
);
|
||||
case FutureStatus.rejected:
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text('Failed to get channels'),
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: AlertMessage(
|
||||
message: 'Failed to get channels',
|
||||
icon: Icons.error,
|
||||
),
|
||||
),
|
||||
);
|
||||
case FutureStatus.fulfilled:
|
||||
|
|
@ -98,9 +107,8 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
),
|
||||
)
|
||||
: null,
|
||||
subtitle: channel.isLive
|
||||
? Text('Uptime: ${DateTime.now().difference(DateTime.parse(channel.startedAt)).toString().split('.')[0]}')
|
||||
: null,
|
||||
subtitle:
|
||||
channel.isLive ? Text('Uptime: ${DateTime.now().difference(DateTime.parse(channel.startedAt)).toString().split('.')[0]}') : null,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ abstract class CategoriesStoreBase with Store {
|
|||
|
||||
_error = null;
|
||||
} on SocketException {
|
||||
_error = 'Failed to connect :(';
|
||||
_error = 'Failed to connect';
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ abstract class ListStoreBase with Store {
|
|||
|
||||
_error = null;
|
||||
} on SocketException {
|
||||
_error = 'Failed to connect :(';
|
||||
_error = 'Failed to connect';
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:frosty/api/twitch_api.dart';
|
|||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/screens/home/stores/categories_store.dart';
|
||||
import 'package:frosty/screens/home/widgets/category_card.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -33,7 +34,10 @@ class _CategoriesState extends State<Categories> with AutomaticKeepAliveClientMi
|
|||
|
||||
if (_categoriesStore.error != null) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text(_categoriesStore.error!),
|
||||
content: AlertMessage(
|
||||
message: _categoriesStore.error!,
|
||||
icon: Icons.error,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
|
|
@ -44,7 +48,7 @@ class _CategoriesState extends State<Categories> with AutomaticKeepAliveClientMi
|
|||
child: Observer(
|
||||
builder: (_) {
|
||||
if (_categoriesStore.categories.isEmpty && _categoriesStore.isLoading && _categoriesStore.error == null) {
|
||||
return const LoadingIndicator(subtitle: Text('Loading categories...'));
|
||||
return const LoadingIndicator(subtitle: 'Loading categories...');
|
||||
}
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:frosty/models/category.dart';
|
|||
import 'package:frosty/screens/home/stores/list_store.dart';
|
||||
import 'package:frosty/screens/home/widgets/stream_card.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -45,7 +46,10 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
|
||||
if (widget.listStore.error != null) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text(widget.listStore.error!),
|
||||
content: AlertMessage(
|
||||
message: widget.listStore.error!,
|
||||
icon: Icons.error,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
|
|
@ -103,10 +107,8 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
),
|
||||
if (widget.listStore.streams.isEmpty && widget.listStore.isLoading && widget.listStore.error == null)
|
||||
const SliverFillRemaining(
|
||||
child: Center(
|
||||
child: LoadingIndicator(
|
||||
subtitle: Text('Loading streams...'),
|
||||
),
|
||||
child: LoadingIndicator(
|
||||
subtitle: 'Loading streams...',
|
||||
),
|
||||
)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:frosty/core/auth/auth_store.dart';
|
|||
import 'package:frosty/screens/home/stores/list_store.dart';
|
||||
import 'package:frosty/screens/home/widgets/stream_card.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -49,7 +50,10 @@ class _StreamsListState extends State<StreamsList> with AutomaticKeepAliveClient
|
|||
|
||||
if (_listStore.error != null) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text(_listStore.error!),
|
||||
content: AlertMessage(
|
||||
message: _listStore.error!,
|
||||
icon: Icons.error,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
|
|
@ -59,9 +63,18 @@ class _StreamsListState extends State<StreamsList> with AutomaticKeepAliveClient
|
|||
},
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
if (_listStore.streams.isEmpty && _listStore.isLoading && _listStore.error == null) {
|
||||
return const LoadingIndicator(subtitle: Text('Loading streams...'));
|
||||
if (_listStore.isLoading && _listStore.error == null) {
|
||||
return const Center(
|
||||
child: LoadingIndicator(subtitle: 'Loading streams...'),
|
||||
);
|
||||
}
|
||||
|
||||
if (_listStore.streams.isEmpty) {
|
||||
return const Center(
|
||||
child: AlertMessage(message: 'No followed streams'),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/block_button.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
|
|
@ -161,7 +162,8 @@ class ProfileCard extends StatelessWidget {
|
|||
builder: (context) {
|
||||
if (authStore.error != null) {
|
||||
return ListTile(
|
||||
title: const Text('Failed to Connect'),
|
||||
leading: const Icon(Icons.error),
|
||||
title: const Text('Failed to connect'),
|
||||
trailing: Button(
|
||||
onPressed: authStore.init,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
|
|
@ -234,7 +236,9 @@ class BlockedUsers extends StatelessWidget {
|
|||
builder: (context) {
|
||||
if (authStore.user.blockedUsers.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('You don\'t have any blocked users.'),
|
||||
child: AlertMessage(
|
||||
message: 'No blocked users',
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
|
@ -32,11 +34,15 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
|
||||
widget.settingsStore.resetAllSettings();
|
||||
|
||||
Navigator.pop(context);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Settings reset!'),
|
||||
content: AlertMessage(message: 'All settings reset'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
|
@ -111,12 +117,14 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
icon: const Icon(Icons.delete_sweep),
|
||||
child: const Text('Clear Image Cache'),
|
||||
onPressed: () async {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
await DefaultCacheManager().emptyCache();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Image cache cleared!'),
|
||||
content: AlertMessage(message: 'Image cache cleared'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
34
lib/widgets/alert_message.dart
Normal file
34
lib/widgets/alert_message.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A simple widget that displays an alert message in the center.
|
||||
class AlertMessage extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
|
||||
const AlertMessage({
|
||||
Key? key,
|
||||
required this.message,
|
||||
final this.icon = Icons.info,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class LoadingIndicator extends StatelessWidget {
|
||||
final Widget? subtitle;
|
||||
final String? subtitle;
|
||||
final double spacing;
|
||||
|
||||
const LoadingIndicator({Key? key, this.subtitle, this.spacing = 10.0}) : super(key: key);
|
||||
|
|
@ -11,15 +11,19 @@ class LoadingIndicator extends StatelessWidget {
|
|||
if (subtitle == null) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator.adaptive(),
|
||||
SizedBox(height: spacing),
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator.adaptive(),
|
||||
SizedBox(height: spacing),
|
||||
Text(
|
||||
subtitle!,
|
||||
],
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue