diff --git a/lib/models/irc.dart b/lib/models/irc.dart index ce027904..9c78a4dd 100644 --- a/lib/models/irc.dart +++ b/lib/models/irc.dart @@ -123,7 +123,6 @@ class IRCMessage { void Function()? onTapName, void Function(String)? onTapPingedUser, bool showMessage = true, - bool useReadableColors = false, Map? 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(); } } diff --git a/lib/screens/channel/chat/stores/chat_store.dart b/lib/screens/channel/chat/stores/chat_store.dart index 392f1587..977dfc52 100644 --- a/lib/screens/channel/chat/stores/chat_store.dart +++ b/lib/screens/channel/chat/stores/chat_store.dart @@ -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. diff --git a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart index 9455975b..739de108 100644 --- a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart +++ b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart @@ -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, ), diff --git a/lib/screens/channel/chat/widgets/chat_message.dart b/lib/screens/channel/chat/widgets/chat_message.dart index 67a5ebd2..4b211fe4 100644 --- a/lib/screens/channel/chat/widgets/chat_message.dart +++ b/lib/screens/channel/chat/widgets/chat_message.dart @@ -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: diff --git a/lib/screens/channel/video/video_bar.dart b/lib/screens/channel/video/video_bar.dart index f2a5e031..17b160ad 100644 --- a/lib/screens/channel/video/video_bar.dart +++ b/lib/screens/channel/video/video_bar.dart @@ -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, diff --git a/lib/screens/channel/video/video_overlay.dart b/lib/screens/channel/video/video_overlay.dart index 225e1267..4cea5337 100644 --- a/lib/screens/channel/video/video_overlay.dart +++ b/lib/screens/channel/video/video_overlay.dart @@ -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, diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index ce76080e..1de2a408 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -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 { 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 { ], ), body: SafeArea( + top: false, + bottom: false, child: Observer( builder: (_) => IndexedStack( index: _homeStore.selectedIndex, @@ -108,9 +115,15 @@ class _HomeState extends State { 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 { ), ), ), - 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 { 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 { onDestinationSelected: _homeStore.handleTap, ), ), - ], + ), ), ), ); diff --git a/lib/screens/home/search/search.dart b/lib/screens/home/search/search.dart index 13c579ce..e2d3b6be 100644 --- a/lib/screens/home/search/search.dart +++ b/lib/screens/home/search/search.dart @@ -33,126 +33,133 @@ class _SearchState extends State { @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), - ], - ); - }, + }, + ), ), ), - ), - ], + ], + ), ); } diff --git a/lib/screens/home/stream_list/stream_card.dart b/lib/screens/home/stream_list/stream_card.dart index 847da42a..1e73d859 100644 --- a/lib/screens/home/stream_list/stream_card.dart +++ b/lib/screens/home/stream_list/stream_card.dart @@ -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, diff --git a/lib/screens/home/stream_list/streams_list.dart b/lib/screens/home/stream_list/streams_list.dart index 91e1c1c1..0bcdf050 100644 --- a/lib/screens/home/stream_list/streams_list.dart +++ b/lib/screens/home/stream_list/streams_list.dart @@ -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 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 final settingsStore = context.watch(); - 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 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 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 ); }, ), + SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom, + ), + ), ], ), ), diff --git a/lib/screens/home/top/categories/categories.dart b/lib/screens/home/top/categories/categories.dart index 863906ff..19e483a5 100644 --- a/lib/screens/home/top/categories/categories.dart +++ b/lib/screens/home/top/categories/categories.dart @@ -54,6 +54,7 @@ class _CategoriesState extends State _categoriesStore.checkLastTimeRefreshedAndUpdate(); return RefreshIndicator.adaptive( + edgeOffset: MediaQuery.of(context).padding.top, onRefresh: () async { HapticFeedback.lightImpact(); await _categoriesStore.refreshCategories(); diff --git a/lib/screens/home/top/top.dart b/lib/screens/home/top/top.dart deleted file mode 100644 index 4cae1112..00000000 --- a/lib/screens/home/top/top.dart +++ /dev/null @@ -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 createState() => _TopSectionState(); -} - -class _TopSectionState extends State - 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(); - } -} diff --git a/lib/screens/settings/chat_settings.dart b/lib/screens/settings/chat_settings.dart index 2cd1ab43..59a2eebc 100644 --- a/lib/screens/settings/chat_settings.dart +++ b/lib/screens/settings/chat_settings.dart @@ -31,7 +31,6 @@ class _ChatSettingsState extends State { return Observer( builder: (context) => ListView( - padding: const EdgeInsets.only(top: 16), children: [ const SectionHeader( 'Message sizing', @@ -146,14 +145,6 @@ class _ChatSettingsState extends State { 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 { 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( diff --git a/lib/screens/settings/general_settings.dart b/lib/screens/settings/general_settings.dart index 4c5b347c..5dfb13aa 100644 --- a/lib/screens/settings/general_settings.dart +++ b/lib/screens/settings/general_settings.dart @@ -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', diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index 38f121cd..91f3b203 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -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()), diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index fe408e5b..a217303d 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -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 = []; 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; diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart index 54ff13bd..e2496dc7 100644 --- a/lib/screens/settings/stores/settings_store.g.dart +++ b/lib/screens/settings/stores/settings_store.g.dart @@ -18,7 +18,6 @@ SettingsStore _$SettingsStoreFromJson(Map 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 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 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 _$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 _$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 _$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}, diff --git a/lib/screens/settings/video_settings.dart b/lib/screens/settings/video_settings.dart index c9f6fe16..68cf2594 100644 --- a/lib/screens/settings/video_settings.dart +++ b/lib/screens/settings/video_settings.dart @@ -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', diff --git a/lib/screens/settings/widgets/settings_tile_route.dart b/lib/screens/settings/widgets/settings_tile_route.dart index 52c1a94e..ef86c2d8 100644 --- a/lib/screens/settings/widgets/settings_tile_route.dart +++ b/lib/screens/settings/widgets/settings_tile_route.dart @@ -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, ), diff --git a/lib/theme.dart b/lib/theme.dart index 32ddc46e..459c8ab8 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -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; - } } diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index b758fc95..2aeeb1f6 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -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? 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, ); } diff --git a/lib/widgets/section_header.dart b/lib/widgets/section_header.dart index baf41a1e..8635075f 100644 --- a/lib/widgets/section_header.dart +++ b/lib/widgets/section_header.dart @@ -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), + ), ), ); }