Refresh themes and styling (#394)
* consolidate themes * update and tweak styles, add border styling * add borders between video and chat * tweak heights * improve reply threads - taller new design - can now tap parent's reply - fix crash when tapping self * tweak border colors * tweak theme * tweak theme * tweak theme * add accent color picker in settings * overhaul colors and styles * remove print * add divider to chat user modal and revert height * increase deleted message opacity * revert highlight color * tweak color * tweak colors * tweak padding * tweak padding * tweak placeholder color * revert input fill color * use error color
This commit is contained in:
parent
29cd8ee455
commit
f68de827cd
41 changed files with 721 additions and 477 deletions
|
|
@ -124,21 +124,23 @@ class _MyAppState extends State<MyApp> {
|
|||
return Observer(
|
||||
builder: (context) {
|
||||
final settingsStore = context.read<SettingsStore>();
|
||||
final themes = FrostyThemes();
|
||||
final themes =
|
||||
FrostyThemes(colorSchemeSeed: Color(settingsStore.accentColor));
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Frosty',
|
||||
theme: themes.light,
|
||||
darkTheme: settingsStore.themeType == ThemeType.black
|
||||
? themes.black
|
||||
: themes.dark,
|
||||
themeMode: settingsStore.themeType == ThemeType.system
|
||||
? ThemeMode.system
|
||||
: settingsStore.themeType == ThemeType.light
|
||||
? ThemeMode.light
|
||||
: ThemeMode.dark,
|
||||
home: widget.firstRun ? const OnboardingIntro() : const Home(),
|
||||
navigatorKey: navigatorKey,
|
||||
return Provider<FrostyThemes>(
|
||||
create: (_) => themes,
|
||||
child: MaterialApp(
|
||||
title: 'Frosty',
|
||||
theme: themes.light,
|
||||
darkTheme: themes.dark,
|
||||
themeMode: settingsStore.themeType == ThemeType.system
|
||||
? ThemeMode.system
|
||||
: settingsStore.themeType == ThemeType.light
|
||||
? ThemeMode.light
|
||||
: ThemeMode.dark,
|
||||
home: widget.firstRun ? const OnboardingIntro() : const Home(),
|
||||
navigatorKey: navigatorKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -245,11 +245,17 @@ class _VideoChatState extends State<VideoChat> {
|
|||
visible: settingsStore.fullScreenChatOverlay,
|
||||
maintainState: true,
|
||||
child: Theme(
|
||||
data: FrostyThemes().dark,
|
||||
data: FrostyThemes(
|
||||
colorSchemeSeed: Color(settingsStore.accentColor),
|
||||
).dark,
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context)
|
||||
.style
|
||||
.copyWith(color: Colors.white),
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: context
|
||||
.watch<FrostyThemes>()
|
||||
.dark
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
child: landscapeChat,
|
||||
),
|
||||
),
|
||||
|
|
@ -281,15 +287,31 @@ class _VideoChatState extends State<VideoChat> {
|
|||
if (settingsStore.showOverlay)
|
||||
Row(
|
||||
children: settingsStore.landscapeChatLeftSide
|
||||
? [overlayChat, Expanded(child: overlay)]
|
||||
: [Expanded(child: overlay), overlayChat],
|
||||
? [
|
||||
overlayChat,
|
||||
const VerticalDivider(),
|
||||
Expanded(child: overlay),
|
||||
]
|
||||
: [
|
||||
Expanded(child: overlay),
|
||||
const VerticalDivider(),
|
||||
overlayChat,
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: settingsStore.landscapeChatLeftSide
|
||||
? [landscapeChat, Expanded(child: video)]
|
||||
: [Expanded(child: video), landscapeChat],
|
||||
? [
|
||||
landscapeChat,
|
||||
const VerticalDivider(),
|
||||
Expanded(child: video),
|
||||
]
|
||||
: [
|
||||
Expanded(child: video),
|
||||
const VerticalDivider(),
|
||||
landscapeChat,
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [appBar, Expanded(child: chat)],
|
||||
|
|
@ -307,8 +329,10 @@ class _VideoChatState extends State<VideoChat> {
|
|||
children: [
|
||||
if (!settingsStore.showVideo)
|
||||
appBar
|
||||
else
|
||||
else ...[
|
||||
AspectRatio(aspectRatio: 16 / 9, child: video),
|
||||
const Divider(),
|
||||
],
|
||||
Expanded(child: chat),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -147,14 +147,14 @@ class ChatDetails extends StatelessWidget {
|
|||
final children = [
|
||||
const SectionHeader(
|
||||
'Chat modes',
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 4),
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
isFirst: true,
|
||||
),
|
||||
ListTile(
|
||||
title: ChatModes(roomState: chatDetailsStore.roomState),
|
||||
),
|
||||
const SectionHeader(
|
||||
'More',
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people_outline),
|
||||
|
|
@ -162,15 +162,12 @@ class ChatDetails extends StatelessWidget {
|
|||
onTap: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore: chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: userLogin,
|
||||
),
|
||||
builder: (context) => GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore: chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: userLogin,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ class ChatModes extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final disabledColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.5);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
|
@ -23,7 +26,7 @@ class ChatModes extends StatelessWidget {
|
|||
roomState.emoteOnly != '0'
|
||||
? Icons.emoji_emotions_rounded
|
||||
: Icons.emoji_emotions_outlined,
|
||||
color: roomState.emoteOnly != '0' ? Colors.yellow : Colors.grey,
|
||||
color: roomState.emoteOnly != '0' ? Colors.yellow : disabledColor,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
|
|
@ -39,7 +42,7 @@ class ChatModes extends StatelessWidget {
|
|||
roomState.followersOnly != '-1'
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_outline_rounded,
|
||||
color: roomState.followersOnly != '-1' ? Colors.red : Colors.grey,
|
||||
color: roomState.followersOnly != '-1' ? Colors.red : disabledColor,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
|
|
@ -51,7 +54,7 @@ class ChatModes extends StatelessWidget {
|
|||
child: Text(
|
||||
'R9K',
|
||||
style: TextStyle(
|
||||
color: roomState.r9k != '0' ? Colors.purple : Colors.grey,
|
||||
color: roomState.r9k != '0' ? Colors.purple : disabledColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
|
@ -66,7 +69,7 @@ class ChatModes extends StatelessWidget {
|
|||
roomState.slowMode != '0'
|
||||
? Icons.hourglass_top_rounded
|
||||
: Icons.hourglass_empty_rounded,
|
||||
color: roomState.slowMode != '0' ? Colors.blue : Colors.grey,
|
||||
color: roomState.slowMode != '0' ? Colors.blue : disabledColor,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
|
|
@ -78,7 +81,7 @@ class ChatModes extends StatelessWidget {
|
|||
roomState.subMode != '0'
|
||||
? Icons.monetization_on_rounded
|
||||
: Icons.monetization_on_outlined,
|
||||
color: roomState.subMode != '0' ? Colors.green : Colors.grey,
|
||||
color: roomState.subMode != '0' ? Colors.green : disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
|||
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/animated_scroll_border.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -30,153 +31,162 @@ class ChattersList extends StatefulWidget {
|
|||
class _ChattersListState extends State<ChattersList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return TextField(
|
||||
controller: widget.chatDetailsStore.textController,
|
||||
focusNode: widget.chatDetailsStore.textFieldFocusNode,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.filter_list_rounded),
|
||||
hintText: 'Filter chatters',
|
||||
suffixIcon: widget
|
||||
.chatDetailsStore.textFieldFocusNode.hasFocus ||
|
||||
widget.chatDetailsStore.filterText.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: widget.chatDetailsStore.filterText.isEmpty
|
||||
? 'Cancel'
|
||||
: 'Clear',
|
||||
onPressed: () {
|
||||
if (widget.chatDetailsStore.filterText.isEmpty) {
|
||||
widget.chatDetailsStore.textFieldFocusNode
|
||||
.unfocus();
|
||||
}
|
||||
widget.chatDetailsStore.textController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
Scrollbar(
|
||||
controller: widget.chatDetailsStore.scrollController,
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: widget.chatDetailsStore.scrollController,
|
||||
slivers: [
|
||||
if (widget.chatDetailsStore.filterText.isEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'${NumberFormat().format(widget.chatDetailsStore.chatUsers.length)} chatters found',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.chatDetailsStore.chatUsers.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: AlertMessage(message: 'No chatters found'),
|
||||
)
|
||||
else if (widget
|
||||
.chatDetailsStore.filteredUsers.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child:
|
||||
AlertMessage(message: 'No matching chatters'),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Text(
|
||||
widget.chatDetailsStore.filteredUsers
|
||||
.elementAt(index),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final userInfo =
|
||||
await context.read<TwitchApi>().getUser(
|
||||
headers: context
|
||||
.read<AuthStore>()
|
||||
.headersTwitch,
|
||||
userLogin: widget.chatDetailsStore
|
||||
.filteredUsers
|
||||
.elementAt(index),
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName: userInfo.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
childCount: widget
|
||||
.chatDetailsStore.filteredUsers.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: widget.chatDetailsStore.showJumpButton
|
||||
? ScrollToTopButton(
|
||||
scrollController:
|
||||
widget.chatDetailsStore.scrollController,
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return TextField(
|
||||
controller: widget.chatDetailsStore.textController,
|
||||
focusNode: widget.chatDetailsStore.textFieldFocusNode,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.filter_list_rounded),
|
||||
hintText: 'Filter chatters',
|
||||
suffixIcon: widget
|
||||
.chatDetailsStore.textFieldFocusNode.hasFocus ||
|
||||
widget.chatDetailsStore.filterText.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: widget.chatDetailsStore.filterText.isEmpty
|
||||
? 'Cancel'
|
||||
: 'Clear',
|
||||
onPressed: () {
|
||||
if (widget.chatDetailsStore.filterText.isEmpty) {
|
||||
widget.chatDetailsStore.textFieldFocusNode
|
||||
.unfocus();
|
||||
}
|
||||
widget.chatDetailsStore.textController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
AnimatedScrollBorder(
|
||||
scrollController: widget.chatDetailsStore.scrollController,
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator.adaptive(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
Scrollbar(
|
||||
controller: widget.chatDetailsStore.scrollController,
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: widget.chatDetailsStore.scrollController,
|
||||
slivers: [
|
||||
if (widget.chatDetailsStore.filterText.isEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'${NumberFormat().format(widget.chatDetailsStore.chatUsers.length)} chatters found',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.chatDetailsStore.chatUsers.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child:
|
||||
AlertMessage(message: 'No chatters found'),
|
||||
)
|
||||
else if (widget
|
||||
.chatDetailsStore.filteredUsers.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: AlertMessage(
|
||||
message: 'No matching chatters',
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Text(
|
||||
widget.chatDetailsStore.filteredUsers
|
||||
.elementAt(index),
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
final userInfo = await context
|
||||
.read<TwitchApi>()
|
||||
.getUser(
|
||||
headers: context
|
||||
.read<AuthStore>()
|
||||
.headersTwitch,
|
||||
userLogin: widget
|
||||
.chatDetailsStore.filteredUsers
|
||||
.elementAt(index),
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName: userInfo.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
childCount: widget
|
||||
.chatDetailsStore.filteredUsers.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: widget.chatDetailsStore.showJumpButton
|
||||
? ScrollToTopButton(
|
||||
scrollController:
|
||||
widget.chatDetailsStore.scrollController,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -703,7 +703,7 @@ abstract class ChatStoreBase with Store {
|
|||
// Set the new notification message and create a new timer that will dismiss it after 2 seconds.
|
||||
_notification = notificationMessage;
|
||||
_notificationTimer =
|
||||
Timer(const Duration(seconds: 2), () => _notification = null);
|
||||
Timer(const Duration(seconds: 3), () => _notification = null);
|
||||
}
|
||||
|
||||
/// Updates the sleep timer with the given [duration].
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:frosty/constants.dart';
|
|||
import 'package:frosty/models/irc.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_details.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/animated_scroll_border.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
|
||||
class ChatBottomBar extends StatelessWidget {
|
||||
|
|
@ -70,6 +71,7 @@ class ChatBottomBar extends StatelessWidget {
|
|||
|
||||
return Column(
|
||||
children: [
|
||||
AnimatedScrollBorder(scrollController: chatStore.scrollController),
|
||||
if (chatStore.replyingToMessage != null) ...[
|
||||
const Divider(),
|
||||
ListTile(
|
||||
|
|
|
|||
|
|
@ -8,19 +8,20 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
|||
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/reply_thread.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/theme.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ChatMessage extends StatelessWidget {
|
||||
final IRCMessage ircMessage;
|
||||
final ChatStore chatStore;
|
||||
final bool isModal;
|
||||
final bool showReplyHeader;
|
||||
|
||||
const ChatMessage({
|
||||
super.key,
|
||||
required this.ircMessage,
|
||||
required this.chatStore,
|
||||
this.isModal = false,
|
||||
this.showReplyHeader = true,
|
||||
});
|
||||
|
||||
void onTapName(BuildContext context) {
|
||||
|
|
@ -179,7 +180,7 @@ class ChatMessage extends StatelessWidget {
|
|||
|
||||
Widget? messageHeaderIcon;
|
||||
Widget? messageHeader;
|
||||
if (replyUser != null && replyBody != null) {
|
||||
if (replyUser != null && replyBody != null && showReplyHeader) {
|
||||
messageHeaderIcon = Icon(
|
||||
Icons.chat_rounded,
|
||||
size: messageHeaderIconSize,
|
||||
|
|
@ -221,7 +222,7 @@ class ChatMessage extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
} else if (shouldHighlightMessage) {
|
||||
highlightColor = FrostyThemes.purple;
|
||||
highlightColor = const Color(0xff9146ff);
|
||||
messageHeader = Text(
|
||||
'Highlighted message',
|
||||
style: TextStyle(
|
||||
|
|
@ -265,7 +266,7 @@ class ChatMessage extends StatelessWidget {
|
|||
final banDuration = ircMessage.tags['ban-duration'];
|
||||
|
||||
renderMessage = Opacity(
|
||||
opacity: 0.4,
|
||||
opacity: 0.5,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -315,7 +316,7 @@ class ChatMessage extends StatelessWidget {
|
|||
break;
|
||||
case Command.userNotice:
|
||||
if (chatStore.settings.showUserNotices) {
|
||||
highlightColor = FrostyThemes.purple;
|
||||
highlightColor = const Color(0xff9146ff);
|
||||
Widget? messageHeaderIcon;
|
||||
Widget? messageHeader;
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ class _ChatUserModalState extends State<ChatUserModal> {
|
|||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
|
|
|
|||
|
|
@ -28,67 +28,73 @@ class ReplyThread extends StatelessWidget {
|
|||
final replyUserLogin = selectedMessage.tags['reply-parent-user-login'];
|
||||
final replyBody = selectedMessage.tags['reply-parent-msg-body'];
|
||||
|
||||
final replyName = getReadableName(replyDisplayName!, replyUserLogin!);
|
||||
final replyName = replyUserLogin != null
|
||||
? getReadableName(replyDisplayName!, replyUserLogin)
|
||||
: replyDisplayName;
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(chatStore.settings.messageScale),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context)
|
||||
.style
|
||||
.copyWith(fontSize: chatStore.settings.fontSize),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Reply thread',
|
||||
padding: EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
),
|
||||
if (replyParent != null)
|
||||
ChatMessage(
|
||||
isModal: true,
|
||||
ircMessage: replyParent,
|
||||
chatStore: chatStore,
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
child: Text(
|
||||
'Replies to @$replyName: $replyBody',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(chatStore.settings.messageScale),
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context)
|
||||
.style
|
||||
.copyWith(fontSize: chatStore.settings.fontSize),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Replies',
|
||||
isFirst: true,
|
||||
padding: EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
),
|
||||
if (replyParent != null)
|
||||
ChatMessage(
|
||||
ircMessage: replyParent,
|
||||
chatStore: chatStore,
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||
child: Text(
|
||||
'$replyName: $replyBody',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Flexible(
|
||||
child: ListView(
|
||||
primary: false,
|
||||
children: chatStore.messages
|
||||
.where(
|
||||
(message) =>
|
||||
message.tags['reply-parent-msg-id'] ==
|
||||
selectedMessage.tags['reply-parent-msg-id'],
|
||||
)
|
||||
.map(
|
||||
(message) => ChatMessage(
|
||||
isModal: true,
|
||||
showReplyHeader: false,
|
||||
ircMessage: message,
|
||||
chatStore: chatStore,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
children: chatStore.messages
|
||||
.where(
|
||||
(message) =>
|
||||
message.tags['reply-parent-msg-id'] ==
|
||||
selectedMessage.tags['reply-parent-msg-id'],
|
||||
)
|
||||
.map(
|
||||
(message) => ChatMessage(
|
||||
isModal: true,
|
||||
ircMessage: message,
|
||||
chatStore: chatStore,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
|||
import 'package:frosty/screens/channel/video/video_bar.dart';
|
||||
import 'package:frosty/screens/channel/video/video_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/theme.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:frosty/widgets/uptime.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// Creates a widget containing controls which enable interactions with an underlying [Video] widget.
|
||||
class VideoOverlay extends StatelessWidget {
|
||||
|
|
@ -30,11 +32,14 @@ class VideoOverlay extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
|
||||
final surfaceColor =
|
||||
context.watch<FrostyThemes>().dark.colorScheme.onSurface;
|
||||
|
||||
final backButton = IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back_rounded,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
);
|
||||
|
|
@ -49,13 +54,13 @@ class VideoOverlay extends StatelessWidget {
|
|||
icon: videoStore.settingsStore.fullScreenChatOverlay
|
||||
? const Icon(Icons.chat_rounded)
|
||||
: const Icon(Icons.chat_outlined),
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
);
|
||||
|
||||
final videoSettingsButton = IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
onPressed: () {
|
||||
videoStore.updateStreamQualities();
|
||||
showModalBottomSheet(
|
||||
|
|
@ -106,19 +111,19 @@ class VideoOverlay extends StatelessWidget {
|
|||
Observer(
|
||||
builder: (context) => Text(
|
||||
videoStore.latency ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: surfaceColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.speed_rounded,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -129,9 +134,9 @@ class VideoOverlay extends StatelessWidget {
|
|||
message: 'Refresh',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
Icons.refresh_rounded,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
onPressed: videoStore.handleRefresh,
|
||||
),
|
||||
|
|
@ -147,7 +152,7 @@ class VideoOverlay extends StatelessWidget {
|
|||
videoStore.settingsStore.fullScreen
|
||||
? Icons.fullscreen_exit_rounded
|
||||
: Icons.fullscreen_rounded,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
onPressed: () => videoStore.settingsStore.fullScreen =
|
||||
!videoStore.settingsStore.fullScreen,
|
||||
|
|
@ -160,9 +165,9 @@ class VideoOverlay extends StatelessWidget {
|
|||
: 'Exit landscape mode',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
Icons.screen_rotation_rounded,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
onPressed: () {
|
||||
if (orientation == Orientation.portrait) {
|
||||
|
|
@ -224,8 +229,8 @@ class VideoOverlay extends StatelessWidget {
|
|||
Expanded(
|
||||
child: VideoBar(
|
||||
streamInfo: streamInfo,
|
||||
titleTextColor: Colors.white,
|
||||
subtitleTextColor: Colors.white,
|
||||
titleTextColor: surfaceColor,
|
||||
subtitleTextColor: surfaceColor,
|
||||
subtitleTextWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
|
@ -245,7 +250,7 @@ class VideoOverlay extends StatelessWidget {
|
|||
videoStore.paused
|
||||
? Icons.play_arrow_rounded
|
||||
: Icons.pause_rounded,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
onPressed: videoStore.handlePausePlay,
|
||||
),
|
||||
|
|
@ -273,8 +278,8 @@ class VideoOverlay extends StatelessWidget {
|
|||
const SizedBox(width: 4),
|
||||
Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: surfaceColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
|
@ -289,36 +294,32 @@ class VideoOverlay extends StatelessWidget {
|
|||
onTap: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => SizedBox(
|
||||
height:
|
||||
MediaQuery.of(context).size.height * 0.8,
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore:
|
||||
chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: streamInfo.userLogin,
|
||||
),
|
||||
builder: (context) => GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore:
|
||||
chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: streamInfo.userLogin,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.visibility,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
NumberFormat().format(
|
||||
videoStore.streamInfo?.viewerCount,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: surfaceColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFeatures: [
|
||||
fontFeatures: const [
|
||||
FontFeature.tabularFigures(),
|
||||
],
|
||||
),
|
||||
|
|
@ -338,9 +339,9 @@ class VideoOverlay extends StatelessWidget {
|
|||
message: 'Enter picture-in-picture',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
Icons.picture_in_picture_alt_rounded,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
onPressed: videoStore.requestPictureInPicture,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ class _HomeState extends State<Home> {
|
|||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
shape: const Border(),
|
||||
title: Observer(
|
||||
builder: (_) {
|
||||
final titles = [
|
||||
|
|
@ -73,19 +74,22 @@ class _HomeState extends State<Home> {
|
|||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: isLoggedIn
|
||||
? ProfilePicture(
|
||||
userLogin: _authStore.user.details!.login,
|
||||
radius: 16,
|
||||
)
|
||||
: const Icon(Icons.settings_rounded),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
Settings(settingsStore: context.read<SettingsStore>()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: isLoggedIn
|
||||
? ProfilePicture(
|
||||
userLogin: _authStore.user.details!.login,
|
||||
radius: 16,
|
||||
)
|
||||
: const Icon(Icons.settings_rounded),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
Settings(settingsStore: context.read<SettingsStore>()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -111,31 +115,44 @@ class _HomeState extends State<Home> {
|
|||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Observer(
|
||||
builder: (_) => NavigationBar(
|
||||
destinations: [
|
||||
if (_authStore.isLoggedIn)
|
||||
NavigationDestination(
|
||||
icon: _homeStore.selectedIndex == 0
|
||||
? const Icon(Icons.favorite_rounded)
|
||||
: const Icon(Icons.favorite_border_rounded),
|
||||
label: 'Following',
|
||||
tooltip: 'Followed streams',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.arrow_upward_rounded),
|
||||
label: 'Top',
|
||||
tooltip: 'Top streams and categories',
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Divider(),
|
||||
Observer(
|
||||
builder: (_) => NavigationBar(
|
||||
destinations: [
|
||||
if (_authStore.isLoggedIn)
|
||||
const NavigationDestination(
|
||||
icon: Icon(
|
||||
Icons.favorite_border_rounded,
|
||||
),
|
||||
selectedIcon: Icon(Icons.favorite_rounded),
|
||||
label: 'Following',
|
||||
tooltip: 'Following',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(
|
||||
Icons.arrow_upward_rounded,
|
||||
),
|
||||
selectedIcon: Icon(Icons.arrow_upward_rounded),
|
||||
label: 'Top',
|
||||
tooltip: 'Top',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(
|
||||
Icons.search_rounded,
|
||||
),
|
||||
selectedIcon: Icon(Icons.search_rounded),
|
||||
label: 'Search',
|
||||
tooltip: 'Search',
|
||||
),
|
||||
],
|
||||
selectedIndex: _homeStore.selectedIndex,
|
||||
onDestinationSelected: _homeStore.handleTap,
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.search_rounded),
|
||||
label: 'Search',
|
||||
tooltip: 'Search for channels and categories',
|
||||
),
|
||||
],
|
||||
selectedIndex: _homeStore.selectedIndex,
|
||||
onDestinationSelected: _homeStore.handleTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:frosty/screens/home/search/search_results_channels.dart';
|
|||
import 'package:frosty/screens/home/search/search_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/animated_scroll_border.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ class _SearchState extends State<Search> {
|
|||
Observer(
|
||||
builder: (context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TextField(
|
||||
controller: _searchStore.textEditingController,
|
||||
focusNode: _searchStore.textFieldFocusNode,
|
||||
|
|
@ -66,6 +67,7 @@ class _SearchState extends State<Search> {
|
|||
);
|
||||
},
|
||||
),
|
||||
AnimatedScrollBorder(scrollController: widget.scrollController),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: widget.scrollController,
|
||||
|
|
@ -87,6 +89,7 @@ class _SearchState extends State<Search> {
|
|||
const SectionHeader(
|
||||
'History',
|
||||
padding: EdgeInsets.zero,
|
||||
isFirst: true,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _searchStore.searchHistory.clear,
|
||||
|
|
@ -129,7 +132,7 @@ class _SearchState extends State<Search> {
|
|||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Channels',
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
isFirst: true,
|
||||
),
|
||||
),
|
||||
SearchResultsChannels(
|
||||
|
|
@ -139,7 +142,6 @@ class _SearchState extends State<Search> {
|
|||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Categories',
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
),
|
||||
SearchResultsCategories(searchStore: _searchStore),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:frosty/models/stream.dart';
|
|||
import 'package:frosty/screens/channel/channel.dart';
|
||||
import 'package:frosty/screens/channel/video/video_bar.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/theme.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
|
|
@ -41,9 +42,11 @@ class LargeStreamCard extends StatelessWidget {
|
|||
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final thumbnailWidth = min((size.width * pixelRatio) ~/ 1, 1920);
|
||||
final thumbnailHeight = min((thumbnailWidth * (9 / 16)).toInt(), 1080);
|
||||
final surfaceColor =
|
||||
context.watch<FrostyThemes>().dark.colorScheme.onSurface;
|
||||
|
||||
final thumbnail = ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
children: [
|
||||
|
|
@ -68,7 +71,7 @@ class LargeStreamCard extends StatelessWidget {
|
|||
) +
|
||||
cacheUrlExtension,
|
||||
placeholder: (context, url) => ColoredBox(
|
||||
color: Colors.grey.shade900,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: const LoadingIndicator(),
|
||||
),
|
||||
useOldImageOnUrlChange: true,
|
||||
|
|
@ -92,8 +95,8 @@ class LargeStreamCard extends StatelessWidget {
|
|||
const SizedBox(width: 4),
|
||||
Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: surfaceColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
|
@ -106,16 +109,16 @@ class LargeStreamCard extends StatelessWidget {
|
|||
preferBelow: false,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.visibility,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
color: surfaceColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
NumberFormat().format(streamInfo.viewerCount),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
style: TextStyle(
|
||||
color: surfaceColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:frosty/models/stream.dart';
|
|||
import 'package:frosty/screens/channel/channel.dart';
|
||||
import 'package:frosty/screens/home/top/categories/category_streams.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/theme.dart';
|
||||
import 'package:frosty/utils.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
|
|
@ -54,7 +55,7 @@ class StreamCard extends StatelessWidget {
|
|||
) +
|
||||
cacheUrlExtension,
|
||||
placeholder: (context, url) => ColoredBox(
|
||||
color: Colors.grey.shade900,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: const LoadingIndicator(),
|
||||
),
|
||||
useOldImageOnUrlChange: true,
|
||||
|
|
@ -97,10 +98,10 @@ class StreamCard extends StatelessWidget {
|
|||
margin: const EdgeInsets.all(4),
|
||||
child: Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
color: context.watch<FrostyThemes>().dark.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:frosty/screens/home/top/categories/category_card.dart';
|
|||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/animated_scroll_border.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -136,6 +137,10 @@ class _StreamsListState extends State<StreamsList>
|
|||
category: _listStore.categoryDetails!,
|
||||
isTappable: false,
|
||||
),
|
||||
if (_listStore.scrollController != null)
|
||||
AnimatedScrollBorder(
|
||||
scrollController: _listStore.scrollController!,
|
||||
),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: _listStore.scrollController,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:frosty/screens/home/top/categories/categories_store.dart';
|
|||
import 'package:frosty/screens/home/top/categories/category_card.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/animated_scroll_border.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
|
@ -99,23 +100,32 @@ class _CategoriesState extends State<Categories>
|
|||
);
|
||||
}
|
||||
|
||||
return Scrollbar(
|
||||
controller: widget.scrollController,
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: widget.scrollController,
|
||||
itemCount: _categoriesStore.categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > _categoriesStore.categories.length - 10 &&
|
||||
_categoriesStore.hasMore) {
|
||||
_categoriesStore.getCategories();
|
||||
}
|
||||
return CategoryCard(
|
||||
key: ValueKey(_categoriesStore.categories[index].id),
|
||||
category: _categoriesStore.categories[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
AnimatedScrollBorder(
|
||||
scrollController: widget.scrollController,
|
||||
),
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: widget.scrollController,
|
||||
child: ListView.builder(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: widget.scrollController,
|
||||
itemCount: _categoriesStore.categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > _categoriesStore.categories.length - 10 &&
|
||||
_categoriesStore.hasMore) {
|
||||
_categoriesStore.getCategories();
|
||||
}
|
||||
return CategoryCard(
|
||||
key: ValueKey(_categoriesStore.categories[index].id),
|
||||
category: _categoriesStore.categories[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class CategoryCard extends StatelessWidget {
|
|||
'${artWidth}x$artHeight.jpg',
|
||||
),
|
||||
placeholder: (context, url) => ColoredBox(
|
||||
color: Colors.grey.shade900,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: const LoadingIndicator(),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/screens/home/stream_list/stream_list_store.dart';
|
||||
import 'package:frosty/screens/home/stream_list/streams_list.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
|
||||
/// A widget that displays a list of streams under the provided [categoryId].
|
||||
class CategoryStreams extends StatelessWidget {
|
||||
|
|
@ -16,8 +15,13 @@ class CategoryStreams extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const FrostyAppBar(
|
||||
title: SizedBox(),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: Icon(Icons.adaptive.arrow_back_rounded),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
shape: const Border(),
|
||||
),
|
||||
body: StreamsList(
|
||||
listType: ListType.category,
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ class ProfileCard extends StatelessWidget {
|
|||
builder: (context) {
|
||||
if (authStore.error != null) {
|
||||
return ListTile(
|
||||
leading: const Icon(
|
||||
leading: Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: Colors.red,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
title: const Text('Failed to connect'),
|
||||
trailing: FilledButton.tonal(
|
||||
|
|
|
|||
|
|
@ -30,12 +30,15 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
|
||||
return Observer(
|
||||
builder: (context) => ListView(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
children: [
|
||||
const SectionHeader('Message sizing'),
|
||||
const SectionHeader(
|
||||
'Message sizing',
|
||||
isFirst: true,
|
||||
),
|
||||
ExpansionTile(
|
||||
title: const Text(
|
||||
'Preview',
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
children: [
|
||||
Container(
|
||||
|
|
@ -141,7 +144,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
divisions: 15,
|
||||
onChanged: (newValue) => settingsStore.fontSize = newValue,
|
||||
),
|
||||
const SectionHeader('Message appearance', showDivider: true),
|
||||
const SectionHeader('Message appearance'),
|
||||
SettingsListSwitch(
|
||||
title: 'Use readable name colors',
|
||||
subtitle: const Text(
|
||||
|
|
@ -172,7 +175,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
onChanged: (newValue) => settingsStore.timestampType =
|
||||
TimestampType.values[timestampNames.indexOf(newValue)],
|
||||
),
|
||||
const SectionHeader('Delay and latency', showDivider: true),
|
||||
const SectionHeader('Delay and latency'),
|
||||
SettingsListSwitch(
|
||||
title: 'Sync message delay with stream latency (experimental)',
|
||||
value: settingsStore.autoSyncChatDelay,
|
||||
|
|
@ -189,7 +192,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
divisions: 30,
|
||||
onChanged: (newValue) => settingsStore.chatDelay = newValue,
|
||||
),
|
||||
const SectionHeader('Alerts', showDivider: true),
|
||||
const SectionHeader('Alerts'),
|
||||
SettingsListSwitch(
|
||||
title: 'Highlight first time chatters',
|
||||
value: settingsStore.highlightFirstTimeChatter,
|
||||
|
|
@ -204,7 +207,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
value: settingsStore.showUserNotices,
|
||||
onChanged: (newValue) => settingsStore.showUserNotices = newValue,
|
||||
),
|
||||
const SectionHeader('Layout', showDivider: true),
|
||||
const SectionHeader('Layout'),
|
||||
SettingsListSwitch(
|
||||
title: 'Show bottom bar',
|
||||
value: settingsStore.showBottomBar,
|
||||
|
|
@ -225,7 +228,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
onChanged: (newValue) =>
|
||||
settingsStore.chatNotificationsOnBottom = newValue,
|
||||
),
|
||||
const SectionHeader('Landscape mode', showDivider: true),
|
||||
const SectionHeader('Landscape mode'),
|
||||
SettingsListSwitch(
|
||||
title: 'Move chat left',
|
||||
value: settingsStore.landscapeChatLeftSide,
|
||||
|
|
@ -271,7 +274,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
onChanged: (newValue) =>
|
||||
settingsStore.fullScreenChatOverlayOpacity = newValue,
|
||||
),
|
||||
const SectionHeader('Sleep', showDivider: true),
|
||||
const SectionHeader('Sleep'),
|
||||
SettingsListSwitch(
|
||||
title: 'Prevent sleep in chat-only mode',
|
||||
subtitle: const Text(
|
||||
|
|
@ -281,7 +284,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
onChanged: (newValue) =>
|
||||
settingsStore.chatOnlyPreventSleep = newValue,
|
||||
),
|
||||
const SectionHeader('Autocomplete', showDivider: true),
|
||||
const SectionHeader('Autocomplete'),
|
||||
SettingsListSwitch(
|
||||
title: 'Show autocomplete bar',
|
||||
subtitle: const Text(
|
||||
|
|
@ -290,7 +293,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
value: settingsStore.autocomplete,
|
||||
onChanged: (newValue) => settingsStore.autocomplete = newValue,
|
||||
),
|
||||
const SectionHeader('Emotes and badges', showDivider: true),
|
||||
const SectionHeader('Emotes and badges'),
|
||||
SettingsListSwitch(
|
||||
title: 'Show Twitch emotes',
|
||||
value: settingsStore.showTwitchEmotes,
|
||||
|
|
@ -326,7 +329,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
value: settingsStore.showFFZBadges,
|
||||
onChanged: (newValue) => settingsStore.showFFZBadges = newValue,
|
||||
),
|
||||
const SectionHeader('Recent messages', showDivider: true),
|
||||
const SectionHeader('Recent messages'),
|
||||
SettingsListSwitch(
|
||||
title: 'Show historical recent messages',
|
||||
subtitle: Text.rich(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_select.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class GeneralSettings extends StatelessWidget {
|
||||
|
|
@ -14,16 +16,63 @@ class GeneralSettings extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) => ListView(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
children: [
|
||||
const SectionHeader('Display'),
|
||||
const SectionHeader(
|
||||
'Theme',
|
||||
isFirst: true,
|
||||
),
|
||||
SettingsListSelect(
|
||||
title: 'Theme',
|
||||
selectedOption: themeNames[settingsStore.themeType.index],
|
||||
options: themeNames,
|
||||
onChanged: (newTheme) => settingsStore.themeType =
|
||||
ThemeType.values[themeNames.indexOf(newTheme)],
|
||||
),
|
||||
const SectionHeader('Stream card', showDivider: true),
|
||||
ListTile(
|
||||
title: const Text('Accent color'),
|
||||
trailing: IconButton(
|
||||
icon: DecoratedBox(
|
||||
position: DecorationPosition.foreground,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Color(settingsStore.accentColor),
|
||||
radius: 16,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Accent color',
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: Color(settingsStore.accentColor),
|
||||
onColorChanged: (newColor) =>
|
||||
settingsStore.accentColor = newColor.value,
|
||||
enableAlpha: false,
|
||||
pickerAreaBorderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
labelTypes: const [],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Done'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SectionHeader('Stream card'),
|
||||
SettingsListSwitch(
|
||||
title: 'Use large stream card',
|
||||
value: settingsStore.largeStreamCard,
|
||||
|
|
@ -34,7 +83,7 @@ class GeneralSettings extends StatelessWidget {
|
|||
value: settingsStore.showThumbnails,
|
||||
onChanged: (newValue) => settingsStore.showThumbnails = newValue,
|
||||
),
|
||||
const SectionHeader('Links', showDivider: true),
|
||||
const SectionHeader('Links'),
|
||||
SettingsListSwitch(
|
||||
title: 'Open links in external browser',
|
||||
value: settingsStore.launchUrlExternal,
|
||||
|
|
|
|||
|
|
@ -37,25 +37,32 @@ class Settings extends StatelessWidget {
|
|||
),
|
||||
icon: const Icon(SimpleIcons.buymeacoffee),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'View source on GitHub',
|
||||
onPressed: () => launchUrl(
|
||||
Uri.parse('https://github.com/tommyxchow/frosty'),
|
||||
mode: settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppBrowserView,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
tooltip: 'View source on GitHub',
|
||||
onPressed: () => launchUrl(
|
||||
Uri.parse('https://github.com/tommyxchow/frosty'),
|
||||
mode: settingsStore.launchUrlExternal
|
||||
? LaunchMode.externalApplication
|
||||
: LaunchMode.inAppBrowserView,
|
||||
),
|
||||
icon: const Icon(SimpleIcons.github),
|
||||
),
|
||||
icon: const Icon(SimpleIcons.github),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
children: [
|
||||
const SectionHeader('Profile'),
|
||||
const SectionHeader(
|
||||
'Profile',
|
||||
isFirst: true,
|
||||
),
|
||||
ProfileCard(authStore: context.read<AuthStore>()),
|
||||
const SectionHeader('Options', showDivider: true),
|
||||
const SectionHeader('Customize'),
|
||||
SettingsTileRoute(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: 'General',
|
||||
|
|
@ -71,7 +78,7 @@ class Settings extends StatelessWidget {
|
|||
title: 'Chat',
|
||||
child: ChatSettings(settingsStore: settingsStore),
|
||||
),
|
||||
const SectionHeader('Other', showDivider: true),
|
||||
const SectionHeader('Other'),
|
||||
OtherSettings(settingsStore: settingsStore),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
// * General Settings
|
||||
// Theme defaults
|
||||
static const defaultThemeType = ThemeType.system;
|
||||
static const defaultAccentColor = 0xff9146ff;
|
||||
|
||||
// Stream card defaults
|
||||
static const defaultShowThumbnails = true;
|
||||
|
|
@ -29,6 +30,10 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var themeType = defaultThemeType;
|
||||
|
||||
@JsonKey(defaultValue: defaultAccentColor)
|
||||
@observable
|
||||
var accentColor = defaultAccentColor;
|
||||
|
||||
// Stream card options
|
||||
@JsonKey(defaultValue: defaultShowThumbnails)
|
||||
@observable
|
||||
|
|
@ -46,6 +51,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
@action
|
||||
void resetGeneralSettings() {
|
||||
themeType = defaultThemeType;
|
||||
accentColor = defaultAccentColor;
|
||||
|
||||
largeStreamCard = defaultLargeStreamCard;
|
||||
showThumbnails = defaultShowThumbnails;
|
||||
|
|
@ -379,13 +385,12 @@ abstract class _SettingsStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
const themeNames = ['System', 'Light', 'Dark', 'Black'];
|
||||
const themeNames = ['System', 'Light', 'Dark'];
|
||||
|
||||
enum ThemeType {
|
||||
system,
|
||||
light,
|
||||
dark,
|
||||
black,
|
||||
}
|
||||
|
||||
const timestampNames = ['Disabled', '12-hour', '24-hour'];
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..themeType = $enumDecodeNullable(_$ThemeTypeEnumMap, json['themeType'],
|
||||
unknownValue: ThemeType.system) ??
|
||||
ThemeType.system
|
||||
..accentColor = (json['accentColor'] as num?)?.toInt() ?? 4287710975
|
||||
..showThumbnails = json['showThumbnails'] as bool? ?? true
|
||||
..largeStreamCard = json['largeStreamCard'] as bool? ?? false
|
||||
..launchUrlExternal = json['launchUrlExternal'] as bool? ?? false
|
||||
|
|
@ -71,6 +72,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
||||
<String, dynamic>{
|
||||
'themeType': _$ThemeTypeEnumMap[instance.themeType]!,
|
||||
'accentColor': instance.accentColor,
|
||||
'showThumbnails': instance.showThumbnails,
|
||||
'largeStreamCard': instance.largeStreamCard,
|
||||
'launchUrlExternal': instance.launchUrlExternal,
|
||||
|
|
@ -122,7 +124,6 @@ const _$ThemeTypeEnumMap = {
|
|||
ThemeType.system: 'system',
|
||||
ThemeType.light: 'light',
|
||||
ThemeType.dark: 'dark',
|
||||
ThemeType.black: 'black',
|
||||
};
|
||||
|
||||
const _$TimestampTypeEnumMap = {
|
||||
|
|
@ -161,6 +162,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$accentColorAtom =
|
||||
Atom(name: '_SettingsStoreBase.accentColor', context: context);
|
||||
|
||||
@override
|
||||
int get accentColor {
|
||||
_$accentColorAtom.reportRead();
|
||||
return super.accentColor;
|
||||
}
|
||||
|
||||
@override
|
||||
set accentColor(int value) {
|
||||
_$accentColorAtom.reportWrite(value, super.accentColor, () {
|
||||
super.accentColor = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showThumbnailsAtom =
|
||||
Atom(name: '_SettingsStoreBase.showThumbnails', context: context);
|
||||
|
||||
|
|
@ -951,6 +968,7 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
String toString() {
|
||||
return '''
|
||||
themeType: ${themeType},
|
||||
accentColor: ${accentColor},
|
||||
showThumbnails: ${showThumbnails},
|
||||
largeStreamCard: ${largeStreamCard},
|
||||
launchUrlExternal: ${launchUrlExternal},
|
||||
|
|
|
|||
|
|
@ -17,8 +17,12 @@ class VideoSettings extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) => ListView(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
children: [
|
||||
const SectionHeader('Player'),
|
||||
const SectionHeader(
|
||||
'Player',
|
||||
isFirst: true,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Enable video',
|
||||
value: settingsStore.showVideo,
|
||||
|
|
@ -36,7 +40,7 @@ class VideoSettings extends StatelessWidget {
|
|||
value: settingsStore.showLatency,
|
||||
onChanged: (newValue) => settingsStore.showLatency = newValue,
|
||||
),
|
||||
const SectionHeader('Overlay', showDivider: true),
|
||||
const SectionHeader('Overlay'),
|
||||
SettingsListSwitch(
|
||||
title: 'Use custom video overlay',
|
||||
subtitle: const Text(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
/// A custom-styled adaptive [ListTile] with options to select.
|
||||
class SettingsListSelect extends StatelessWidget {
|
||||
final String title;
|
||||
final String? title;
|
||||
final String? subtitle;
|
||||
final String selectedOption;
|
||||
final List<String> options;
|
||||
|
|
@ -10,7 +10,7 @@ class SettingsListSelect extends StatelessWidget {
|
|||
|
||||
const SettingsListSelect({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
required this.selectedOption,
|
||||
required this.options,
|
||||
|
|
@ -20,9 +20,11 @@ class SettingsListSelect extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(title),
|
||||
title: title != null ? Text(title!) : null,
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: title != null
|
||||
? const EdgeInsets.symmetric(vertical: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: SegmentedButton(
|
||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
segments: options
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A custom-styled adaptive [Slider].
|
||||
|
|
@ -24,37 +23,37 @@ class SettingsListSlider extends StatelessWidget {
|
|||
this.divisions,
|
||||
});
|
||||
|
||||
static const _textStyle = TextStyle(fontWeight: FontWeight.w500);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(title, style: _textStyle),
|
||||
Text(title),
|
||||
const Spacer(),
|
||||
Text(
|
||||
trailing,
|
||||
style: _textStyle.copyWith(
|
||||
fontFeatures: [const FontFeature.tabularFigures()],
|
||||
style: const TextStyle(
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
if (subtitle != null) Text(subtitle!),
|
||||
],
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
if (subtitle != null) Text(subtitle!),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ class SettingsListSwitch extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile.adaptive(
|
||||
isThreeLine: subtitle != null,
|
||||
title: Text(title),
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
|
|
|
|||
|
|
@ -3,22 +3,28 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FrostyThemes {
|
||||
static const gray = Color.fromRGBO(18, 18, 18, 1);
|
||||
static const purple = Color(0xff9146ff);
|
||||
final Color colorSchemeSeed;
|
||||
|
||||
const FrostyThemes({required this.colorSchemeSeed});
|
||||
|
||||
ThemeData createBaseTheme({
|
||||
required Brightness brightness,
|
||||
required Color colorSchemeSeed,
|
||||
Color? backgroundColor,
|
||||
}) {
|
||||
final secondaryBackground = brightness == Brightness.light
|
||||
? Colors.grey.shade200
|
||||
: Colors.grey.shade800;
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: colorSchemeSeed,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final borderColor = colorScheme.outlineVariant;
|
||||
|
||||
const borderWidth = 0.5;
|
||||
|
||||
return ThemeData(
|
||||
fontFamily: 'Inter',
|
||||
brightness: brightness,
|
||||
colorSchemeSeed: colorSchemeSeed,
|
||||
colorScheme: colorScheme,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
scaffoldBackgroundColor: backgroundColor,
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
|
|
@ -34,25 +40,31 @@ class FrostyThemes {
|
|||
elevation: 0,
|
||||
backgroundColor: backgroundColor,
|
||||
surfaceTintColor: backgroundColor,
|
||||
shape: Border(
|
||||
bottom: BorderSide(color: borderColor, width: borderWidth),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
borderSide: BorderSide(color: secondaryBackground),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainer,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
borderSide: BorderSide(color: secondaryBackground),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
borderSide: BorderSide(color: secondaryBackground),
|
||||
disabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.all(Radius.circular(100)),
|
||||
),
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
elevation: 0,
|
||||
backgroundColor: backgroundColor,
|
||||
height: 64,
|
||||
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
|
||||
),
|
||||
tabBarTheme: const TabBarTheme(
|
||||
dividerColor: Colors.transparent,
|
||||
|
|
@ -60,28 +72,29 @@ class FrostyThemes {
|
|||
),
|
||||
tooltipTheme: TooltipThemeData(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(color: secondaryBackground),
|
||||
border: Border.all(color: borderColor, width: borderWidth),
|
||||
),
|
||||
textStyle: TextStyle(
|
||||
color: brightness == Brightness.dark ? Colors.white : Colors.black,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
showCloseIcon: true,
|
||||
backgroundColor: backgroundColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: secondaryBackground),
|
||||
side: BorderSide(color: borderColor),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
dividerTheme: const DividerThemeData(
|
||||
thickness: 0.5,
|
||||
space: 0.5,
|
||||
dividerTheme: DividerThemeData(
|
||||
thickness: borderWidth,
|
||||
space: borderWidth,
|
||||
color: borderColor,
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
// Used in alert dialog title.
|
||||
|
|
@ -112,7 +125,6 @@ class FrostyThemes {
|
|||
// Used in list tile title.
|
||||
bodyLarge: TextStyle(
|
||||
letterSpacing: 0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
letterSpacing: 0,
|
||||
|
|
@ -127,8 +139,8 @@ class FrostyThemes {
|
|||
ThemeData get light {
|
||||
final theme = createBaseTheme(
|
||||
brightness: Brightness.light,
|
||||
colorSchemeSeed: purple,
|
||||
backgroundColor: Colors.white,
|
||||
colorSchemeSeed: colorSchemeSeed,
|
||||
backgroundColor: const Color.fromRGBO(248, 248, 248, 1),
|
||||
);
|
||||
|
||||
return theme;
|
||||
|
|
@ -137,17 +149,7 @@ class FrostyThemes {
|
|||
ThemeData get dark {
|
||||
final theme = createBaseTheme(
|
||||
brightness: Brightness.dark,
|
||||
colorSchemeSeed: purple,
|
||||
backgroundColor: gray,
|
||||
);
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
ThemeData get black {
|
||||
final theme = createBaseTheme(
|
||||
brightness: Brightness.dark,
|
||||
colorSchemeSeed: purple,
|
||||
colorSchemeSeed: colorSchemeSeed,
|
||||
backgroundColor: Colors.black,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,21 +15,23 @@ class AlertMessage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.5);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment:
|
||||
centered ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline_rounded,
|
||||
color: color ?? Colors.grey,
|
||||
color: color ?? defaultColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: color ?? Colors.grey,
|
||||
color: color ?? defaultColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
50
lib/widgets/animated_scroll_border.dart
Normal file
50
lib/widgets/animated_scroll_border.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedScrollBorder extends StatefulWidget {
|
||||
final ScrollController scrollController;
|
||||
|
||||
const AnimatedScrollBorder({
|
||||
super.key,
|
||||
required this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedScrollBorder> createState() => _AnimatedScrollBorderState();
|
||||
}
|
||||
|
||||
class _AnimatedScrollBorderState extends State<AnimatedScrollBorder> {
|
||||
bool _isScrolled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.scrollController.addListener(_updateScrollState);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.scrollController.removeListener(_updateScrollState);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateScrollState() {
|
||||
setState(() {
|
||||
_isScrolled = widget.scrollController.offset > 0;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: _isScrolled
|
||||
? const Divider()
|
||||
: Divider(
|
||||
key: ValueKey(_isScrolled),
|
||||
color: Colors.transparent,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ class FrostyDialog extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog.adaptive(
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
title,
|
||||
// style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
|
|
|
|||
|
|
@ -18,9 +18,8 @@ class LoadingIndicator extends StatelessWidget {
|
|||
SizedBox(height: spacing),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,28 +16,25 @@ class FrostyNotification extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade900,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
margin: const EdgeInsets.all(10),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info_outline_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/theme.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FrostyPhotoViewDialog extends StatefulWidget {
|
||||
final String imageUrl;
|
||||
|
|
@ -34,7 +36,10 @@ class _FrostyPhotoViewDialogState extends State<FrostyPhotoViewDialog> {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: context.watch<FrostyThemes>().dark.colorScheme.onSurface,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class ProfilePicture extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final diameter = radius * 2;
|
||||
|
||||
final placeholderColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
return ClipOval(
|
||||
child: FutureBuilder(
|
||||
future: context.read<TwitchApi>().getUser(
|
||||
|
|
@ -32,10 +34,10 @@ class ProfilePicture extends StatelessWidget {
|
|||
height: diameter,
|
||||
imageUrl: snapshot.data!.profileImageUrl,
|
||||
placeholder: (context, url) =>
|
||||
ColoredBox(color: Colors.grey.shade900),
|
||||
ColoredBox(color: placeholderColor),
|
||||
)
|
||||
: Container(
|
||||
color: Colors.grey.shade900,
|
||||
color: placeholderColor,
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ScrollToTopButton extends StatelessWidget {
|
|||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
),
|
||||
child: const Icon(Icons.arrow_upward_rounded),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,34 +4,37 @@ class SectionHeader extends StatelessWidget {
|
|||
final String text;
|
||||
final EdgeInsets? padding;
|
||||
final double? fontSize;
|
||||
final bool showDivider;
|
||||
final bool isFirst;
|
||||
|
||||
const SectionHeader(
|
||||
this.text, {
|
||||
super.key,
|
||||
this.padding,
|
||||
this.fontSize,
|
||||
this.showDivider = false,
|
||||
this.isFirst = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showDivider) const Divider(),
|
||||
Padding(
|
||||
padding: padding ?? const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: Text(
|
||||
return Padding(
|
||||
padding: padding ?? const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isFirst) ...[
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue