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:
Tommy Chow 2022-06-28 10:32:16 -04:00
parent b0d0f43ec0
commit 3a5f883b1f
19 changed files with 353 additions and 238 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
}

View file

@ -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();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View 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,
),
),
],
);
}
}

View file

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