Merge pull request #152 from tommyxchow/dev
Merge latest changes from dev into 90
This commit is contained in:
commit
9fe06f95bc
11 changed files with 210 additions and 142 deletions
|
|
@ -49,7 +49,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "com.tommychow.frosty"
|
||||
minSdkVersion 19
|
||||
minSdkVersion 26
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const clientId = String.fromEnvironment('CLIENT_ID');
|
|||
const secret = String.fromEnvironment('SECRET');
|
||||
|
||||
/// The tracesSampleRate for sentry crash reporting.
|
||||
const sampleRate = 1.0;
|
||||
const sampleRate = 0.2;
|
||||
|
||||
/// BTTV emotes with zero width to allow for overlaying other emotes.
|
||||
const zeroWidthEmotes = [
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ class Emote {
|
|||
final bool zeroWidth;
|
||||
final String url;
|
||||
final EmoteType type;
|
||||
final String? ownerId;
|
||||
|
||||
const Emote({
|
||||
required this.name,
|
||||
|
|
@ -272,6 +273,7 @@ class Emote {
|
|||
required this.zeroWidth,
|
||||
required this.url,
|
||||
required this.type,
|
||||
this.ownerId,
|
||||
});
|
||||
|
||||
factory Emote.fromTwitch(EmoteTwitch emote, EmoteType type) => Emote(
|
||||
|
|
@ -279,6 +281,7 @@ class Emote {
|
|||
zeroWidth: false,
|
||||
url: 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0',
|
||||
type: type,
|
||||
ownerId: emote.ownerId,
|
||||
);
|
||||
|
||||
factory Emote.fromBTTV(EmoteBTTV emote, EmoteType type) => Emote(
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ Emote _$EmoteFromJson(Map<String, dynamic> json) => Emote(
|
|||
zeroWidth: json['zeroWidth'] as bool,
|
||||
url: json['url'] as String,
|
||||
type: $enumDecode(_$EmoteTypeEnumMap, json['type']),
|
||||
ownerId: json['ownerId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EmoteToJson(Emote instance) => <String, dynamic>{
|
||||
|
|
@ -140,6 +141,7 @@ Map<String, dynamic> _$EmoteToJson(Emote instance) => <String, dynamic>{
|
|||
'zeroWidth': instance.zeroWidth,
|
||||
'url': instance.url,
|
||||
'type': _$EmoteTypeEnumMap[instance.type],
|
||||
'ownerId': instance.ownerId,
|
||||
};
|
||||
|
||||
const _$EmoteTypeEnumMap = {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_panel.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/recent_emotes_panel.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
|
||||
class EmoteMenu extends StatefulWidget {
|
||||
|
|
@ -41,33 +42,55 @@ class _EmoteMenuState extends State<EmoteMenu> {
|
|||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: sections
|
||||
.mapIndexed(
|
||||
(index, section) => Padding(
|
||||
padding: const EdgeInsets.only(right: 5.0),
|
||||
child: Observer(
|
||||
builder: (context) => Button(
|
||||
onPressed: () {
|
||||
_pageContoller.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
widget.chatStore.assetsStore.emoteMenuIndex = index;
|
||||
},
|
||||
color: index == widget.chatStore.assetsStore.emoteMenuIndex ? Theme.of(context).colorScheme.secondary : Colors.grey,
|
||||
child: Text(
|
||||
section,
|
||||
style: const TextStyle(
|
||||
letterSpacing: 0.8,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
children: [
|
||||
...sections.mapIndexed(
|
||||
(index, section) => Padding(
|
||||
padding: const EdgeInsets.only(right: 5.0),
|
||||
child: Observer(
|
||||
builder: (context) => Button(
|
||||
onPressed: () {
|
||||
_pageContoller.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
widget.chatStore.assetsStore.emoteMenuIndex = index;
|
||||
},
|
||||
color: index == widget.chatStore.assetsStore.emoteMenuIndex ? Theme.of(context).colorScheme.secondary : Colors.grey,
|
||||
child: Text(
|
||||
section,
|
||||
style: const TextStyle(
|
||||
letterSpacing: 0.8,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Button(
|
||||
onPressed: () async {
|
||||
await widget.chatStore.getAssets();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: AlertMessage(message: 'Emotes refreshed'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
child: const Text(
|
||||
'REFRESH EMOTES',
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.8,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
|
@ -81,7 +104,7 @@ class _EmoteMenuState extends State<EmoteMenu> {
|
|||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.userEmoteToObject.values.toList(),
|
||||
twitchEmotes: widget.chatStore.assetsStore.userEmoteSectionToEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
|
|
|
|||
|
|
@ -6,78 +6,72 @@ import 'package:frosty/widgets/section_header.dart';
|
|||
|
||||
class EmoteMenuPanel extends StatelessWidget {
|
||||
final ChatStore chatStore;
|
||||
final List<Emote> emotes;
|
||||
final List<Emote>? emotes;
|
||||
final Map<String, List<Emote>>? twitchEmotes;
|
||||
|
||||
const EmoteMenuPanel({
|
||||
Key? key,
|
||||
required this.chatStore,
|
||||
required this.emotes,
|
||||
this.emotes,
|
||||
this.twitchEmotes,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final globalEmotes = emotes
|
||||
.where((emote) =>
|
||||
emote.type == EmoteType.twitchGlobal ||
|
||||
emote.type == EmoteType.bttvGlobal ||
|
||||
emote.type == EmoteType.ffzGlobal ||
|
||||
emote.type == EmoteType.sevenTVGlobal)
|
||||
.toList();
|
||||
if (emotes != null) {
|
||||
final globalEmotes =
|
||||
emotes!.where((emote) => emote.type == EmoteType.bttvGlobal || emote.type == EmoteType.ffzGlobal || emote.type == EmoteType.sevenTVGlobal).toList();
|
||||
|
||||
final channelEmotes = emotes
|
||||
.where((emote) =>
|
||||
emote.type == EmoteType.twitchChannel ||
|
||||
emote.type == EmoteType.bttvChannel ||
|
||||
emote.type == EmoteType.bttvShared ||
|
||||
emote.type == EmoteType.ffzChannel ||
|
||||
emote.type == EmoteType.sevenTVChannel)
|
||||
.toList();
|
||||
final channelEmotes = emotes!
|
||||
.where((emote) =>
|
||||
emote.type == EmoteType.bttvChannel ||
|
||||
emote.type == EmoteType.bttvShared ||
|
||||
emote.type == EmoteType.ffzChannel ||
|
||||
emote.type == EmoteType.sevenTVChannel)
|
||||
.toList();
|
||||
|
||||
final subEmotes = emotes.where((emote) => emote.type == EmoteType.twitchSub).toList();
|
||||
final miscEmotes = emotes.where((emote) => emote.type == EmoteType.twitchUnlocked).toList();
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (globalEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Global Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (globalEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Global Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: globalEmotes,
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: globalEmotes,
|
||||
),
|
||||
],
|
||||
if (channelEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader('Channel Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: channelEmotes,
|
||||
),
|
||||
],
|
||||
],
|
||||
if (channelEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader('Channel Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: channelEmotes,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
for (final entry in twitchEmotes!.entries) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
entry.key,
|
||||
padding: entry.key == 'Global Emotes' ? const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0) : null,
|
||||
),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: entry.value,
|
||||
),
|
||||
]
|
||||
],
|
||||
if (subEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader('Subscribed Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: subEmotes,
|
||||
),
|
||||
],
|
||||
if (miscEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader('Unlocked Emotes'),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: miscEmotes,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,14 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
|
||||
/// The emotes that are "owned" and may be used by the current user.
|
||||
@readonly
|
||||
// ignore: prefer_final_fields
|
||||
var _userEmoteToObject = <String, Emote>{};
|
||||
|
||||
/// The Twitch emote types mapped to their emotes (i.e., global, specific channels, unlocked).
|
||||
@readonly
|
||||
// ignore: prefer_final_fields
|
||||
var _userEmoteSectionToEmotes = <String, List<Emote>>{};
|
||||
|
||||
/// The map of user IDs to their FFZ badges.
|
||||
@readonly
|
||||
var _userToFFZBadges = <String, List<Badge>>{};
|
||||
|
|
@ -91,8 +97,8 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
// Retrieve the instance that will allow us to retrieve local search history.
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
_recentEmotes = prefs.getStringList('recent_emotes')?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson))).toList().asObservable() ??
|
||||
ObservableList<Emote>();
|
||||
_recentEmotes =
|
||||
prefs.getStringList('recent_emotes')?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson))).toList().asObservable() ?? ObservableList<Emote>();
|
||||
|
||||
_disposeReaction = autorun((_) {
|
||||
if (_recentEmotes.length > 48) _recentEmotes.removeLast();
|
||||
|
|
@ -153,10 +159,8 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
required Function onError,
|
||||
}) =>
|
||||
Future.wait([
|
||||
twitchApi
|
||||
.getBadgesGlobal()
|
||||
.then((badges) => twitchBadgesToObject.addAll(badges))
|
||||
.then((_) => twitchApi.getBadgesChannel(id: channelId).then((badges) => twitchBadgesToObject.addAll(badges)).catchError(onError)),
|
||||
twitchApi.getBadgesGlobal().then((badges) => twitchBadgesToObject.addAll(badges)).catchError(onError),
|
||||
twitchApi.getBadgesChannel(id: channelId).then((badges) => twitchBadgesToObject.addAll(badges)).catchError(onError),
|
||||
ffzApi.getBadges().then((badges) => _userToFFZBadges = badges).catchError(onError),
|
||||
sevenTVApi.getBadges().then((badges) => _userTo7TVBadges = badges).catchError(onError),
|
||||
bttvApi.getBadges().then((badges) => _userToBTTVBadges = badges).catchError(onError),
|
||||
|
|
@ -167,10 +171,38 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
required List<String> emoteSets,
|
||||
required Map<String, String> headers,
|
||||
required Function onError,
|
||||
}) =>
|
||||
Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError)))
|
||||
.then((emotes) => emotes.expand((list) => list).toList())
|
||||
.then((userEmotes) => _userEmoteToObject = {for (final emote in userEmotes) emote.name: emote}.asObservable());
|
||||
}) async {
|
||||
final userEmotes = await Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError)));
|
||||
|
||||
for (final emoteSet in userEmotes) {
|
||||
if (emoteSet.isNotEmpty) {
|
||||
if (emoteSet.first.type == EmoteType.twitchSub) {
|
||||
final owner = await twitchApi.getUser(id: emoteSet.first.ownerId, headers: headers);
|
||||
_userEmoteSectionToEmotes.update(
|
||||
owner.displayName,
|
||||
(existingEmoteSet) => [...existingEmoteSet, ...emoteSet],
|
||||
ifAbsent: () => emoteSet,
|
||||
);
|
||||
} else if (emoteSet.first.type == EmoteType.twitchGlobal) {
|
||||
_userEmoteSectionToEmotes.update(
|
||||
'Global Emotes',
|
||||
(existingEmoteSet) => [...existingEmoteSet, ...emoteSet],
|
||||
ifAbsent: () => emoteSet,
|
||||
);
|
||||
} else if (emoteSet.first.type == EmoteType.twitchUnlocked) {
|
||||
_userEmoteSectionToEmotes.update(
|
||||
'Unlocked Emotes',
|
||||
(existingEmoteSet) => [...existingEmoteSet, ...emoteSet],
|
||||
ifAbsent: () => emoteSet,
|
||||
);
|
||||
}
|
||||
|
||||
for (final emote in emoteSet) {
|
||||
_userEmoteToObject[emote.name] = emote;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() => _disposeReaction();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,26 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_userEmoteSectionToEmotesAtom = Atom(
|
||||
name: 'ChatAssetsStoreBase._userEmoteSectionToEmotes', context: context);
|
||||
|
||||
Map<String, List<Emote>> get userEmoteSectionToEmotes {
|
||||
_$_userEmoteSectionToEmotesAtom.reportRead();
|
||||
return super._userEmoteSectionToEmotes;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, List<Emote>> get _userEmoteSectionToEmotes =>
|
||||
userEmoteSectionToEmotes;
|
||||
|
||||
@override
|
||||
set _userEmoteSectionToEmotes(Map<String, List<Emote>> value) {
|
||||
_$_userEmoteSectionToEmotesAtom
|
||||
.reportWrite(value, super._userEmoteSectionToEmotes, () {
|
||||
super._userEmoteSectionToEmotes = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_userToFFZBadgesAtom =
|
||||
Atom(name: 'ChatAssetsStoreBase._userToFFZBadges', context: context);
|
||||
|
||||
|
|
@ -179,6 +199,18 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
return _$initAsyncAction.run(() => super.init());
|
||||
}
|
||||
|
||||
late final _$userEmotesFutureAsyncAction =
|
||||
AsyncAction('ChatAssetsStoreBase.userEmotesFuture', context: context);
|
||||
|
||||
@override
|
||||
Future<void> userEmotesFuture(
|
||||
{required List<String> emoteSets,
|
||||
required Map<String, String> headers,
|
||||
required Function onError}) {
|
||||
return _$userEmotesFutureAsyncAction.run(() => super.userEmotesFuture(
|
||||
emoteSets: emoteSets, headers: headers, onError: onError));
|
||||
}
|
||||
|
||||
late final _$ChatAssetsStoreBaseActionController =
|
||||
ActionController(name: 'ChatAssetsStoreBase', context: context);
|
||||
|
||||
|
|
@ -231,21 +263,6 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> userEmotesFuture(
|
||||
{required List<String> emoteSets,
|
||||
required Map<String, String> headers,
|
||||
required Function onError}) {
|
||||
final _$actionInfo = _$ChatAssetsStoreBaseActionController.startAction(
|
||||
name: 'ChatAssetsStoreBase.userEmotesFuture');
|
||||
try {
|
||||
return super.userEmotesFuture(
|
||||
emoteSets: emoteSets, headers: headers, onError: onError);
|
||||
} finally {
|
||||
_$ChatAssetsStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
|||
|
|
@ -231,19 +231,8 @@ abstract class ChatStoreBase with Store {
|
|||
} else if (message.contains('Welcome, GLHF!')) {
|
||||
_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(
|
||||
channelId: channelId,
|
||||
headers: auth.headersTwitch,
|
||||
onEmoteError: (error) {
|
||||
debugPrint(error.toString());
|
||||
return <Emote>[];
|
||||
},
|
||||
onBadgeError: (error) {
|
||||
debugPrint(error.toString());
|
||||
return <Badge>[];
|
||||
},
|
||||
);
|
||||
getAssets();
|
||||
|
||||
// Reset exponential backoff if successfully connected.
|
||||
_retries = 0;
|
||||
_backoffTime = 0;
|
||||
|
|
@ -251,6 +240,21 @@ abstract class ChatStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch the assets used in chat including badges and emotes.
|
||||
@action
|
||||
Future<void> getAssets() async => assetsStore.assetsFuture(
|
||||
channelId: channelId,
|
||||
headers: auth.headersTwitch,
|
||||
onEmoteError: (error) {
|
||||
debugPrint(error.toString());
|
||||
return <Emote>[];
|
||||
},
|
||||
onBadgeError: (error) {
|
||||
debugPrint(error.toString());
|
||||
return <Badge>[];
|
||||
},
|
||||
);
|
||||
|
||||
/// Re-enables [_autoScroll] and jumps to the latest message.
|
||||
@action
|
||||
void resumeScroll() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
|
|
@ -42,20 +40,15 @@ class _HomeState extends State<Home> {
|
|||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
children: const [
|
||||
Text(
|
||||
'Thank you so much for downloading and trying out Frosty! Hopefully it will make your mobile Twitch viewing experience a little more enjoyable.'),
|
||||
const SizedBox(height: 20.0),
|
||||
const Text(
|
||||
'You can see the full changelog on the app store listing or the "Releases" section on the GitHub repo (link on the top right of settings).'),
|
||||
const SizedBox(height: 20.0),
|
||||
const Text('You can also find the FAQ on the GitHub repo (link in Settings -> Other).'),
|
||||
const SizedBox(height: 20.0),
|
||||
const Text('If you have any issues or feature requests for the app, please open an issue on GitHub.'),
|
||||
if (Platform.isAndroid) ...[
|
||||
const SizedBox(height: 20.0),
|
||||
const Text('NOTE: Due to limitations with the Twitch web player, video streams may not work on Android versions below 7.1.1.'),
|
||||
]
|
||||
SizedBox(height: 20.0),
|
||||
Text('You can see the full changelog on the app store listing or the "Releases" section on the GitHub repo (link on the top right of settings).'),
|
||||
SizedBox(height: 20.0),
|
||||
Text('You can also find the FAQ on the GitHub repo (link in Settings -> Other).'),
|
||||
SizedBox(height: 20.0),
|
||||
Text('If you have any issues or feature requests for the app, please open an issue on GitHub.'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class SectionHeader extends StatelessWidget {
|
||||
final String text;
|
||||
final EdgeInsets padding;
|
||||
final EdgeInsets? padding;
|
||||
final double? fontSize;
|
||||
final bool showDivider;
|
||||
|
||||
const SectionHeader(
|
||||
this.text, {
|
||||
Key? key,
|
||||
this.padding = const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0),
|
||||
this.padding,
|
||||
this.fontSize,
|
||||
this.showDivider = false,
|
||||
}) : super(key: key);
|
||||
|
|
@ -17,7 +17,7 @@ class SectionHeader extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
padding: padding ?? const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue