Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
Tommy Chow
21d388fa02 enhance layout and styling in home screen and app bar 2025-06-26 01:11:14 -04:00
Tommy Chow
2008d2c688 update styles 2025-06-25 21:56:33 -04:00
Tommy Chow
56961c7010 fix pinned streams not showing in other lists 2025-06-18 21:12:03 -04:00
Tommy Chow
29c84858a0 clean up tooltips 2025-06-18 19:47:32 -04:00
Tommy Chow
a2d30ec086 simplify settings 2025-06-18 19:46:57 -04:00
Tommy Chow
60a97e6a24 un-center appbar 2025-06-17 17:06:04 -04:00
22 changed files with 270 additions and 416 deletions

View file

@ -123,7 +123,6 @@ class IRCMessage {
void Function()? onTapName,
void Function(String)? onTapPingedUser,
bool showMessage = true,
bool useReadableColors = false,
Map<String, UserTwitch>? channelIdToUserTwitch,
TimestampType timestamp = TimestampType.disabled,
}) {
@ -370,20 +369,18 @@ class IRCMessage {
int.parse((tags['color'] ?? '#868686').replaceFirst('#', '0xFF')),
);
if (useReadableColors) {
final hsl = HSLColor.fromColor(color);
if (isLightTheme == true) {
if (hsl.lightness >= 0.5) {
color = hsl
.withLightness(hsl.lightness + ((0 - hsl.lightness) * 0.5))
.toColor();
}
} else {
if (hsl.lightness <= 0.5) {
color = hsl
.withLightness(hsl.lightness + ((1 - hsl.lightness) * 0.5))
.toColor();
}
final hsl = HSLColor.fromColor(color);
if (isLightTheme == true) {
if (hsl.lightness >= 0.5) {
color = hsl
.withLightness(hsl.lightness + ((0 - hsl.lightness) * 0.5))
.toColor();
}
} else {
if (hsl.lightness <= 0.5) {
color = hsl
.withLightness(hsl.lightness + ((1 - hsl.lightness) * 0.5))
.toColor();
}
}

View file

@ -167,7 +167,7 @@ abstract class ChatStoreBase with Store {
required this.displayName,
}) {
// Enable wakelock to prevent the chat from sleeping.
if (settings.chatOnlyPreventSleep) WakelockPlus.enable();
WakelockPlus.enable();
// Create a reaction that will reconnect to chat when logging in or out.
// Closing the channel will trigger a reconnect with the new credentials.

View file

@ -87,7 +87,6 @@ class ChatBottomBar extends StatelessWidget {
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useReadableColors: chatStore.settings.useReadableColors,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
),

View file

@ -106,7 +106,6 @@ class ChatMessage extends StatelessWidget {
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useReadableColors: chatStore.settings.useReadableColors,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
channelIdToUserTwitch:
@ -172,7 +171,6 @@ class ChatMessage extends StatelessWidget {
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useReadableColors: chatStore.settings.useReadableColors,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
channelIdToUserTwitch:
@ -305,7 +303,6 @@ class ChatMessage extends StatelessWidget {
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
showMessage: chatStore.settings.showDeletedMessages,
useReadableColors: chatStore.settings.useReadableColors,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
channelIdToUserTwitch:
@ -399,8 +396,6 @@ class ChatMessage extends StatelessWidget {
assetsStore: chatStore.assetsStore,
emoteScale: chatStore.settings.emoteScale,
badgeScale: chatStore.settings.badgeScale,
useReadableColors:
chatStore.settings.useReadableColors,
launchExternal: chatStore.settings.launchUrlExternal,
timestamp: chatStore.settings.timestampType,
channelIdToUserTwitch:

View file

@ -48,7 +48,7 @@ class VideoBar extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Tooltip(
message: 'Streamer: $streamerName',
message: streamerName,
showDuration: const Duration(seconds: 3),
child: Text(
streamerName,
@ -62,7 +62,7 @@ class VideoBar extends StatelessWidget {
),
const SizedBox(height: 2),
Tooltip(
message: 'Title: ${streamInfo.title.trim()}',
message: streamTitle,
showDuration: const Duration(seconds: 5),
child: Text(
streamTitle,
@ -91,8 +91,7 @@ class VideoBar extends StatelessWidget {
)
: null,
child: Tooltip(
message:
'Category: ${streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'None'}',
message: category,
showDuration: const Duration(seconds: 3),
child: Text(
category,

View file

@ -352,9 +352,7 @@ class VideoOverlay extends StatelessWidget {
),
),
),
if (orientation == Orientation.landscape &&
settingsStore.showLatency)
latencyTooltip,
if (orientation == Orientation.landscape) latencyTooltip,
Tooltip(
message: 'Enter picture-in-picture',
preferBelow: false,

View file

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -5,11 +7,12 @@ import 'package:frosty/screens/home/home_store.dart';
import 'package:frosty/screens/home/search/search.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/screens/home/top/top.dart';
import 'package:frosty/screens/home/top/categories/categories.dart';
import 'package:frosty/screens/settings/settings.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/screens/settings/widgets/release_notes.dart';
import 'package:frosty/widgets/app_bar.dart';
import 'package:frosty/widgets/profile_picture.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
@ -63,14 +66,16 @@ class _HomeState extends State<Home> {
return GestureDetector(
onTap: FocusScope.of(context).unfocus,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
shape: const Border(),
extendBodyBehindAppBar: true,
extendBody: true,
appBar: FrostyAppBar(
showBackButton: false,
title: Observer(
builder: (_) {
final titles = [
if (_authStore.isLoggedIn) 'Following',
'Top',
'Top Streams',
'Top Categories',
'Search',
];
@ -100,6 +105,8 @@ class _HomeState extends State<Home> {
],
),
body: SafeArea(
top: false,
bottom: false,
child: Observer(
builder: (_) => IndexedStack(
index: _homeStore.selectedIndex,
@ -108,9 +115,15 @@ class _HomeState extends State<Home> {
StreamsList(
listType: ListType.followed,
scrollController: _homeStore.followedScrollController,
addTopPadding: true,
),
TopSection(
homeStore: _homeStore,
StreamsList(
listType: ListType.top,
scrollController: _homeStore.topSectionScrollControllers[0],
addTopPadding: true,
),
Categories(
scrollController: _homeStore.topSectionScrollControllers[1],
),
Search(
scrollController: _homeStore.searchScrollController,
@ -119,12 +132,15 @@ class _HomeState extends State<Home> {
),
),
),
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
Observer(
bottomNavigationBar: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Observer(
builder: (_) => NavigationBar(
backgroundColor: Theme.of(context)
.colorScheme
.surface
.withValues(alpha: 0.6),
destinations: [
if (_authStore.isLoggedIn)
const NavigationDestination(
@ -140,8 +156,16 @@ class _HomeState extends State<Home> {
Icons.arrow_upward_rounded,
),
selectedIcon: Icon(Icons.arrow_upward_rounded),
label: 'Top',
tooltip: 'Top',
label: 'Top streams',
tooltip: 'Top streams',
),
const NavigationDestination(
icon: Icon(
Icons.gamepad_outlined,
),
selectedIcon: Icon(Icons.gamepad_rounded),
label: 'Top categories',
tooltip: 'Top categories',
),
const NavigationDestination(
icon: Icon(
@ -156,7 +180,7 @@ class _HomeState extends State<Home> {
onDestinationSelected: _homeStore.handleTap,
),
),
],
),
),
),
);

View file

@ -33,126 +33,133 @@ class _SearchState extends State<Search> {
@override
Widget build(BuildContext context) {
return Column(
children: [
Observer(
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchStore.textEditingController,
focusNode: _searchStore.textFieldFocusNode,
autocorrect: false,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search_rounded),
hintText: 'Find a channel or category',
suffixIcon: _searchStore.textFieldFocusNode.hasFocus ||
_searchStore.searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.close_rounded),
tooltip: _searchStore.searchText.isEmpty
? 'Cancel'
: 'Clear',
onPressed: () {
if (_searchStore.searchText.isEmpty) {
_searchStore.textFieldFocusNode.unfocus();
}
_searchStore.textEditingController.clear();
},
)
: null,
return SafeArea(
child: Column(
children: [
Observer(
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchStore.textEditingController,
focusNode: _searchStore.textFieldFocusNode,
autocorrect: false,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search_rounded),
hintText: 'Find a channel or category',
suffixIcon: _searchStore.textFieldFocusNode.hasFocus ||
_searchStore.searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.close_rounded),
tooltip: _searchStore.searchText.isEmpty
? 'Cancel'
: 'Clear',
onPressed: () {
if (_searchStore.searchText.isEmpty) {
_searchStore.textFieldFocusNode.unfocus();
}
_searchStore.textEditingController.clear();
},
)
: null,
),
onSubmitted: _searchStore.handleQuery,
),
onSubmitted: _searchStore.handleQuery,
),
);
},
),
AnimatedScrollBorder(scrollController: widget.scrollController),
Expanded(
child: Scrollbar(
controller: widget.scrollController,
child: Observer(
builder: (context) {
if (_searchStore.textEditingController.text.isEmpty) {
if (_searchStore.searchHistory.isEmpty) {
return const AlertMessage(message: 'No recent searches');
}
);
},
),
AnimatedScrollBorder(scrollController: widget.scrollController),
Expanded(
child: Scrollbar(
controller: widget.scrollController,
child: Observer(
builder: (context) {
if (_searchStore.textEditingController.text.isEmpty) {
if (_searchStore.searchHistory.isEmpty) {
return const AlertMessage(
message: 'No recent searches',
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SectionHeader(
'History',
padding: EdgeInsets.zero,
isFirst: true,
),
TextButton(
onPressed: _searchStore.searchHistory.clear,
child: const Text('Clear'),
),
],
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SectionHeader(
'History',
padding: EdgeInsets.zero,
isFirst: true,
),
TextButton(
onPressed: _searchStore.searchHistory.clear,
child: const Text('Clear'),
),
],
),
),
Expanded(
child: ListView(
controller: widget.scrollController,
children: _searchStore.searchHistory
.mapIndexed(
(index, searchTerm) => ListTile(
leading: const Icon(Icons.history_rounded),
title: Text(searchTerm),
onTap: () {
_searchStore.textEditingController.text =
searchTerm;
_searchStore.handleQuery(searchTerm);
_searchStore
.textEditingController.selection =
TextSelection.fromPosition(
TextPosition(
offset: _searchStore
.textEditingController
.text
.length,
),
);
},
),
)
.toList(),
),
),
],
);
}
return CustomScrollView(
controller: widget.scrollController,
slivers: [
const SliverToBoxAdapter(
child: SectionHeader(
'Channels',
isFirst: true,
padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
),
),
Expanded(
child: ListView(
controller: widget.scrollController,
children: _searchStore.searchHistory
.mapIndexed(
(index, searchTerm) => ListTile(
leading: const Icon(Icons.history_rounded),
title: Text(searchTerm),
onTap: () {
_searchStore.textEditingController.text =
searchTerm;
_searchStore.handleQuery(searchTerm);
_searchStore.textEditingController
.selection = TextSelection.fromPosition(
TextPosition(
offset: _searchStore
.textEditingController.text.length,
),
);
},
),
)
.toList(),
SearchResultsChannels(
searchStore: _searchStore,
query: _searchStore.searchText,
),
const SliverToBoxAdapter(
child: SectionHeader(
'Categories',
),
),
SearchResultsCategories(searchStore: _searchStore),
],
);
}
return CustomScrollView(
controller: widget.scrollController,
slivers: [
const SliverToBoxAdapter(
child: SectionHeader(
'Channels',
isFirst: true,
padding: EdgeInsets.fromLTRB(16, 12, 16, 8),
),
),
SearchResultsChannels(
searchStore: _searchStore,
query: _searchStore.searchText,
),
const SliverToBoxAdapter(
child: SectionHeader(
'Categories',
),
),
SearchResultsCategories(searchStore: _searchStore),
],
);
},
},
),
),
),
),
],
],
),
);
}

View file

@ -68,6 +68,10 @@ class StreamCard extends StatelessWidget {
final streamerName =
getReadableName(streamInfo.userName, streamInfo.userLogin);
final streamTitle = streamInfo.title.trim();
final category =
streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'No Category';
const subFontSize = 14.0;
final fontColor = DefaultTextStyle.of(context).style.color;
@ -126,7 +130,7 @@ class StreamCard extends StatelessWidget {
const SizedBox(width: 4),
Flexible(
child: Tooltip(
message: 'Streamer: $streamerName',
message: streamerName,
preferBelow: false,
child: Text(
streamerName,
@ -143,10 +147,10 @@ class StreamCard extends StatelessWidget {
),
const SizedBox(height: 2),
Tooltip(
message: 'Title: ${streamInfo.title.trim()}',
message: streamTitle,
preferBelow: false,
child: Text(
streamInfo.title.trim(),
streamTitle,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: subFontSize,
@ -168,13 +172,10 @@ class StreamCard extends StatelessWidget {
)
: null,
child: Tooltip(
message:
'Category: ${streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'None'}',
message: category,
preferBelow: false,
child: Text(
streamInfo.gameName.isNotEmpty
? streamInfo.gameName
: 'No Category',
category,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: subFontSize,

View file

@ -9,7 +9,6 @@ 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:frosty/widgets/section_header.dart';
@ -27,6 +26,7 @@ class StreamsList extends StatefulWidget {
final ScrollController? scrollController;
final bool showJumpButton;
final bool addTopPadding;
const StreamsList({
super.key,
@ -34,6 +34,7 @@ class StreamsList extends StatefulWidget {
this.categoryId,
this.scrollController,
this.showJumpButton = false,
this.addTopPadding = false,
});
@override
@ -74,9 +75,13 @@ class _StreamsListState extends State<StreamsList>
Widget build(BuildContext context) {
super.build(context);
final double topPadding =
widget.addTopPadding ? MediaQuery.of(context).padding.top : 0;
_listStore.checkLastTimeRefreshedAndUpdate();
return RefreshIndicator.adaptive(
edgeOffset: topPadding,
onRefresh: () async {
HapticFeedback.lightImpact();
@ -130,12 +135,14 @@ class _StreamsListState extends State<StreamsList>
final settingsStore = context.watch<SettingsStore>();
final unpinnedStreams = _listStore.streams
.where(
(stream) =>
!settingsStore.pinnedChannelIds.contains(stream.userId),
)
.toList();
final unpinnedStreams = widget.listType == ListType.followed
? _listStore.streams
.where(
(stream) =>
!settingsStore.pinnedChannelIds.contains(stream.userId),
)
.toList()
: _listStore.streams;
final isFollowingTab = widget.listType == ListType.followed;
@ -150,10 +157,6 @@ class _StreamsListState extends State<StreamsList>
category: _listStore.categoryDetails!,
isTappable: false,
),
if (_listStore.scrollController != null)
AnimatedScrollBorder(
scrollController: _listStore.scrollController!,
),
Expanded(
child: Scrollbar(
controller: _listStore.scrollController,
@ -161,6 +164,12 @@ class _StreamsListState extends State<StreamsList>
physics: const AlwaysScrollableScrollPhysics(),
controller: _listStore.scrollController,
slivers: [
if (widget.addTopPadding)
SliverPadding(
padding: EdgeInsets.only(
top: topPadding,
),
),
if (isFollowingTab &&
_listStore.pinnedStreams.isNotEmpty) ...[
const SliverToBoxAdapter(
@ -263,6 +272,11 @@ class _StreamsListState extends State<StreamsList>
);
},
),
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom,
),
),
],
),
),

View file

@ -54,6 +54,7 @@ class _CategoriesState extends State<Categories>
_categoriesStore.checkLastTimeRefreshedAndUpdate();
return RefreshIndicator.adaptive(
edgeOffset: MediaQuery.of(context).padding.top,
onRefresh: () async {
HapticFeedback.lightImpact();
await _categoriesStore.refreshCategories();

View file

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:frosty/screens/home/home_store.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/screens/home/top/categories/categories.dart';
class TopSection extends StatefulWidget {
final HomeStore homeStore;
const TopSection({
super.key,
required this.homeStore,
});
@override
State<TopSection> createState() => _TopSectionState();
}
class _TopSectionState extends State<TopSection>
with SingleTickerProviderStateMixin {
late final _tabBarController = TabController(length: 2, vsync: this);
@override
void initState() {
super.initState();
_tabBarController.addListener(
() => widget.homeStore.topSectionCurrentIndex = _tabBarController.index,
);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TabBar(
isScrollable: true,
controller: _tabBarController,
tabs: const [
Tab(
child: Text('Streams'),
),
Tab(child: Text('Categories')),
],
),
Expanded(
child: TabBarView(
controller: _tabBarController,
children: [
StreamsList(
listType: ListType.top,
scrollController:
widget.homeStore.topSectionScrollControllers[0],
),
Categories(
scrollController:
widget.homeStore.topSectionScrollControllers[1],
),
],
),
),
],
);
}
@override
void dispose() {
_tabBarController.dispose();
super.dispose();
}
}

View file

@ -31,7 +31,6 @@ class _ChatSettingsState extends State<ChatSettings> {
return Observer(
builder: (context) => ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Message sizing',
@ -146,14 +145,6 @@ class _ChatSettingsState extends State<ChatSettings> {
onChanged: (newValue) => settingsStore.fontSize = newValue,
),
const SectionHeader('Message appearance'),
SettingsListSwitch(
title: 'Use readable name colors',
subtitle: const Text(
'Adjusts the lightness value of overly bright and dark names.',
),
value: settingsStore.useReadableColors,
onChanged: (newValue) => settingsStore.useReadableColors = newValue,
),
SettingsListSwitch(
title: 'Show deleted messages',
subtitle: const Text(
@ -275,16 +266,6 @@ class _ChatSettingsState extends State<ChatSettings> {
onChanged: (newValue) =>
settingsStore.fullScreenChatOverlayOpacity = newValue,
),
const SectionHeader('Sleep'),
SettingsListSwitch(
title: 'Prevent sleep in chat-only mode',
subtitle: const Text(
'Requires restarting the chat in order to take effect.',
),
value: settingsStore.chatOnlyPreventSleep,
onChanged: (newValue) =>
settingsStore.chatOnlyPreventSleep = newValue,
),
const SectionHeader('Muted keywords'),
SettingsMutedWords(settingsStore: settingsStore),
SettingsListSwitch(

View file

@ -16,7 +16,6 @@ class GeneralSettings extends StatelessWidget {
Widget build(BuildContext context) {
return Observer(
builder: (context) => ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Theme',

View file

@ -23,6 +23,7 @@ class Settings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: FrostyAppBar(
title: const Text('Settings'),
actions: [
@ -53,12 +54,12 @@ class Settings extends StatelessWidget {
],
),
body: SafeArea(
top: false,
bottom: false,
child: ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Profile',
'Account',
isFirst: true,
),
ProfileCard(authStore: context.read<AuthStore>()),

View file

@ -63,7 +63,6 @@ abstract class _SettingsStoreBase with Store {
// Player defaults
static const defaultShowVideo = true;
static const defaultDefaultToHighestQuality = false;
static const defaultShowLatency = true;
static const defaultUseEnhancedRendering = false;
// Overlay defaults
@ -80,10 +79,6 @@ abstract class _SettingsStoreBase with Store {
@observable
var defaultToHighestQuality = defaultDefaultToHighestQuality;
@JsonKey(defaultValue: defaultShowLatency)
@observable
var showLatency = defaultShowLatency;
@JsonKey(defaultValue: defaultUseEnhancedRendering)
@observable
var useEnhancedRendering = defaultUseEnhancedRendering;
@ -105,7 +100,6 @@ abstract class _SettingsStoreBase with Store {
void resetVideoSettings() {
showVideo = defaultShowVideo;
defaultToHighestQuality = defaultDefaultToHighestQuality;
showLatency = defaultShowLatency;
useEnhancedRendering = defaultUseEnhancedRendering;
showOverlay = defaultShowOverlay;
@ -122,7 +116,6 @@ abstract class _SettingsStoreBase with Store {
static const defaultFontSize = 12.0;
// Message appearance defaults
static const defaultUseReadableColors = true;
static const defaultShowDeletedMessages = false;
static const defaultShowChatMessageDividers = false;
static const defaultTimestampType = TimestampType.disabled;
@ -147,9 +140,6 @@ abstract class _SettingsStoreBase with Store {
static const defaultChatWidth = 0.25;
static const defaultFullScreenChatOverlayOpacity = 0.5;
// Sleep defaults
static const defaultChatOnlyPreventSleep = false;
// mute words defaults
static const defaultMutedWords = <String>[];
static const defaultMatchWholeWord = true;
@ -191,10 +181,6 @@ abstract class _SettingsStoreBase with Store {
var fontSize = defaultFontSize;
// Message appearance options
@JsonKey(defaultValue: defaultUseReadableColors)
@observable
var useReadableColors = defaultUseReadableColors;
@JsonKey(defaultValue: defaultShowDeletedMessages)
@observable
var showDeletedMessages = defaultShowDeletedMessages;
@ -262,11 +248,6 @@ abstract class _SettingsStoreBase with Store {
@observable
var fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
// Sleep options
@JsonKey(defaultValue: defaultChatOnlyPreventSleep)
@observable
var chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
// Autocomplete options
@JsonKey(defaultValue: defaultAutocomplete)
@observable
@ -322,7 +303,6 @@ abstract class _SettingsStoreBase with Store {
messageSpacing = defaultMessageSpacing;
fontSize = defaultFontSize;
useReadableColors = defaultUseReadableColors;
showDeletedMessages = defaultShowDeletedMessages;
showChatMessageDividers = defaultShowChatMessageDividers;
timestampType = defaultTimestampType;
@ -343,8 +323,6 @@ abstract class _SettingsStoreBase with Store {
chatWidth = defaultChatWidth;
fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
mutedWords = defaultMutedWords;
matchWholeWord = defaultMatchWholeWord;

View file

@ -18,7 +18,6 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
..showVideo = json['showVideo'] as bool? ?? true
..defaultToHighestQuality =
json['defaultToHighestQuality'] as bool? ?? false
..showLatency = json['showLatency'] as bool? ?? true
..useEnhancedRendering = json['useEnhancedRendering'] as bool? ?? false
..showOverlay = json['showOverlay'] as bool? ?? true
..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false
@ -28,7 +27,6 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
..messageSpacing = (json['messageSpacing'] as num?)?.toDouble() ?? 8.0
..fontSize = (json['fontSize'] as num?)?.toDouble() ?? 12.0
..useReadableColors = json['useReadableColors'] as bool? ?? true
..showDeletedMessages = json['showDeletedMessages'] as bool? ?? false
..showChatMessageDividers =
json['showChatMessageDividers'] as bool? ?? false
@ -54,7 +52,6 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.25
..fullScreenChatOverlayOpacity =
(json['fullScreenChatOverlayOpacity'] as num?)?.toDouble() ?? 0.5
..chatOnlyPreventSleep = json['chatOnlyPreventSleep'] as bool? ?? false
..autocomplete = json['autocomplete'] as bool? ?? true
..showTwitchEmotes = json['showTwitchEmotes'] as bool? ?? true
..showTwitchBadges = json['showTwitchBadges'] as bool? ?? true
@ -87,7 +84,6 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
'launchUrlExternal': instance.launchUrlExternal,
'showVideo': instance.showVideo,
'defaultToHighestQuality': instance.defaultToHighestQuality,
'showLatency': instance.showLatency,
'useEnhancedRendering': instance.useEnhancedRendering,
'showOverlay': instance.showOverlay,
'toggleableOverlay': instance.toggleableOverlay,
@ -97,7 +93,6 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
'messageScale': instance.messageScale,
'messageSpacing': instance.messageSpacing,
'fontSize': instance.fontSize,
'useReadableColors': instance.useReadableColors,
'showDeletedMessages': instance.showDeletedMessages,
'showChatMessageDividers': instance.showChatMessageDividers,
'timestampType': _$TimestampTypeEnumMap[instance.timestampType]!,
@ -114,7 +109,6 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
_$LandscapeCutoutTypeEnumMap[instance.landscapeCutout]!,
'chatWidth': instance.chatWidth,
'fullScreenChatOverlayOpacity': instance.fullScreenChatOverlayOpacity,
'chatOnlyPreventSleep': instance.chatOnlyPreventSleep,
'autocomplete': instance.autocomplete,
'showTwitchEmotes': instance.showTwitchEmotes,
'showTwitchBadges': instance.showTwitchBadges,
@ -271,22 +265,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
});
}
late final _$showLatencyAtom =
Atom(name: '_SettingsStoreBase.showLatency', context: context);
@override
bool get showLatency {
_$showLatencyAtom.reportRead();
return super.showLatency;
}
@override
set showLatency(bool value) {
_$showLatencyAtom.reportWrite(value, super.showLatency, () {
super.showLatency = value;
});
}
late final _$useEnhancedRenderingAtom =
Atom(name: '_SettingsStoreBase.useEnhancedRendering', context: context);
@ -432,22 +410,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
});
}
late final _$useReadableColorsAtom =
Atom(name: '_SettingsStoreBase.useReadableColors', context: context);
@override
bool get useReadableColors {
_$useReadableColorsAtom.reportRead();
return super.useReadableColors;
}
@override
set useReadableColors(bool value) {
_$useReadableColorsAtom.reportWrite(value, super.useReadableColors, () {
super.useReadableColors = value;
});
}
late final _$showDeletedMessagesAtom =
Atom(name: '_SettingsStoreBase.showDeletedMessages', context: context);
@ -696,23 +658,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
});
}
late final _$chatOnlyPreventSleepAtom =
Atom(name: '_SettingsStoreBase.chatOnlyPreventSleep', context: context);
@override
bool get chatOnlyPreventSleep {
_$chatOnlyPreventSleepAtom.reportRead();
return super.chatOnlyPreventSleep;
}
@override
set chatOnlyPreventSleep(bool value) {
_$chatOnlyPreventSleepAtom.reportWrite(value, super.chatOnlyPreventSleep,
() {
super.chatOnlyPreventSleep = value;
});
}
late final _$autocompleteAtom =
Atom(name: '_SettingsStoreBase.autocomplete', context: context);
@ -1034,7 +979,6 @@ largeStreamCard: ${largeStreamCard},
launchUrlExternal: ${launchUrlExternal},
showVideo: ${showVideo},
defaultToHighestQuality: ${defaultToHighestQuality},
showLatency: ${showLatency},
useEnhancedRendering: ${useEnhancedRendering},
showOverlay: ${showOverlay},
toggleableOverlay: ${toggleableOverlay},
@ -1044,7 +988,6 @@ emoteScale: ${emoteScale},
messageScale: ${messageScale},
messageSpacing: ${messageSpacing},
fontSize: ${fontSize},
useReadableColors: ${useReadableColors},
showDeletedMessages: ${showDeletedMessages},
showChatMessageDividers: ${showChatMessageDividers},
timestampType: ${timestampType},
@ -1060,7 +1003,6 @@ landscapeForceVerticalChat: ${landscapeForceVerticalChat},
landscapeCutout: ${landscapeCutout},
chatWidth: ${chatWidth},
fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity},
chatOnlyPreventSleep: ${chatOnlyPreventSleep},
autocomplete: ${autocomplete},
showTwitchEmotes: ${showTwitchEmotes},
showTwitchBadges: ${showTwitchBadges},

View file

@ -17,7 +17,6 @@ class VideoSettings extends StatelessWidget {
Widget build(BuildContext context) {
return Observer(
builder: (context) => ListView(
padding: const EdgeInsets.only(top: 16),
children: [
const SectionHeader(
'Player',
@ -35,11 +34,6 @@ class VideoSettings extends StatelessWidget {
onChanged: (newValue) =>
settingsStore.defaultToHighestQuality = newValue,
),
SettingsListSwitch(
title: 'Show latency',
value: settingsStore.showLatency,
onChanged: (newValue) => settingsStore.showLatency = newValue,
),
if (Platform.isAndroid)
SettingsListSwitch(
title: 'Use enhanced rendering',

View file

@ -25,8 +25,10 @@ class SettingsTileRoute extends StatelessWidget {
MaterialPageRoute(
builder: (context) => useScaffold
? Scaffold(
extendBodyBehindAppBar: true,
appBar: FrostyAppBar(title: Text(title)),
body: SafeArea(
top: false,
bottom: false,
child: child,
),

View file

@ -7,24 +7,29 @@ class FrostyThemes {
const FrostyThemes({required this.colorSchemeSeed});
ThemeData get light => createBaseTheme(
colorScheme: ColorScheme.fromSeed(seedColor: colorSchemeSeed),
backgroundColor: const Color.fromRGBO(248, 248, 248, 1),
);
ThemeData get dark => createBaseTheme(
colorScheme: ColorScheme.fromSeed(
seedColor: colorSchemeSeed,
brightness: Brightness.dark,
),
backgroundColor: Colors.black,
);
ThemeData createBaseTheme({
required Brightness brightness,
required Color colorSchemeSeed,
required ColorScheme colorScheme,
Color? backgroundColor,
}) {
final colorScheme = ColorScheme.fromSeed(
seedColor: colorSchemeSeed,
brightness: brightness,
);
final borderColor = colorScheme.outlineVariant;
const borderWidth = 0.5;
return ThemeData(
fontFamily: 'Inter',
brightness: brightness,
colorScheme: colorScheme,
fontFamily: 'Inter',
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
scaffoldBackgroundColor: backgroundColor,
bottomSheetTheme: BottomSheetThemeData(
@ -37,11 +42,14 @@ class FrostyThemes {
surfaceTintColor: backgroundColor,
),
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: backgroundColor,
backgroundColor: Colors.transparent,
surfaceTintColor: backgroundColor,
shape: Border(
bottom: BorderSide(color: borderColor, width: borderWidth),
),
listTileTheme: ListTileThemeData(
titleTextStyle: TextStyle(
color: colorScheme.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
inputDecorationTheme: InputDecorationTheme(
@ -61,7 +69,6 @@ class FrostyThemes {
),
),
navigationBarTheme: NavigationBarThemeData(
elevation: 0,
backgroundColor: backgroundColor,
height: 64,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
@ -71,6 +78,7 @@ class FrostyThemes {
tabAlignment: TabAlignment.start,
),
tooltipTheme: TooltipThemeData(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: const BorderRadius.all(Radius.circular(8)),
@ -133,24 +141,4 @@ class FrostyThemes {
),
);
}
ThemeData get light {
final theme = createBaseTheme(
brightness: Brightness.light,
colorSchemeSeed: colorSchemeSeed,
backgroundColor: const Color.fromRGBO(248, 248, 248, 1),
);
return theme;
}
ThemeData get dark {
final theme = createBaseTheme(
brightness: Brightness.dark,
colorSchemeSeed: colorSchemeSeed,
backgroundColor: Colors.black,
);
return theme;
}
}

View file

@ -1,28 +1,39 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class FrostyAppBar extends StatelessWidget implements PreferredSizeWidget {
final Widget title;
final bool? centerTitle;
final Widget? title;
final List<Widget>? actions;
final bool showBackButton;
const FrostyAppBar({
super.key,
required this.title,
this.centerTitle,
this.title,
this.actions,
this.showBackButton = true,
});
@override
Widget build(BuildContext context) {
return AppBar(
leading: IconButton(
tooltip: 'Back',
icon: Icon(Icons.adaptive.arrow_back_rounded),
onPressed: Navigator.of(context).pop,
return ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: AppBar(
backgroundColor:
Theme.of(context).colorScheme.surface.withValues(alpha: 0.6),
leading: showBackButton
? IconButton(
tooltip: 'Back',
icon: Icon(Icons.adaptive.arrow_back_rounded),
onPressed: Navigator.of(context).pop,
)
: null,
title: title,
actions: actions,
centerTitle: false,
),
),
title: title,
centerTitle: centerTitle,
actions: actions,
);
}

View file

@ -17,23 +17,17 @@ class SectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
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,
),
),
],
padding: padding ??
(isFirst
? const EdgeInsets.fromLTRB(16, 16, 16, 8)
: const EdgeInsets.fromLTRB(16, 32, 16, 8)),
child: Text(
text.toUpperCase(),
style: TextStyle(
fontSize: fontSize ?? 14,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
);
}