diff --git a/lib/main.dart b/lib/main.dart index df8872e9..acc913fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -57,7 +57,33 @@ class MyApp extends StatelessWidget { final authStore = context.read(); final settingsStore = context.read(); - final defaultTheme = ThemeData( + final lightTheme = ThemeData( + scaffoldBackgroundColor: Colors.white, + splashFactory: Platform.isIOS ? NoSplash.splashFactory : null, + fontFamily: 'Inter', + appBarTheme: const AppBarTheme( + color: Colors.white, + elevation: 0.0, + titleTextStyle: TextStyle( + fontFamily: 'Inter', + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + iconTheme: IconThemeData(color: Colors.black), + ), + colorScheme: ColorScheme.fromSwatch( + primarySwatch: Colors.deepPurple, + accentColor: Colors.deepPurpleAccent, + ), + toggleableActiveColor: Colors.deepPurpleAccent, + tabBarTheme: const TabBarTheme( + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + ), + ); + + final darkTheme = ThemeData( scaffoldBackgroundColor: Colors.grey.shade900, brightness: Brightness.dark, splashFactory: Platform.isIOS ? NoSplash.splashFactory : null, @@ -106,7 +132,13 @@ class MyApp extends StatelessWidget { builder: (context) { return MaterialApp( title: 'Frosty', - theme: settingsStore.useOledTheme ? oledTheme : defaultTheme, + theme: lightTheme, + darkTheme: settingsStore.themeType == ThemeType.dark ? darkTheme : oledTheme, + themeMode: settingsStore.themeType == ThemeType.system + ? ThemeMode.system + : settingsStore.themeType == ThemeType.light + ? ThemeMode.light + : ThemeMode.dark, home: Home( homeStore: HomeStore(), topSectionStore: ListStore( diff --git a/lib/models/irc_message.dart b/lib/models/irc_message.dart index b8f24bc7..4cdfe6c4 100644 --- a/lib/models/irc_message.dart +++ b/lib/models/irc_message.dart @@ -80,6 +80,7 @@ class IRCMessage { bool showMessage = true, bool useZeroWidth = false, bool useReadableColors = false, + bool? isLightTheme, Timestamp timestamp = Timestamp.none, }) { const badgeHeight = 20.0; @@ -235,7 +236,11 @@ class IRCMessage { if (useReadableColors) { final hsl = HSLColor.fromColor(color); - if (hsl.lightness <= 0.5) color = hsl.withLightness(hsl.lightness + ((1 - hsl.lightness) * 0.5)).toColor(); + 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(); + } } // Printing template for debugging purposes. diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart index bdcb6c8f..0bd3aaeb 100644 --- a/lib/screens/channel/chat/chat.dart +++ b/lib/screens/channel/chat/chat.dart @@ -76,7 +76,7 @@ class Chat extends StatelessWidget { builder: (_) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: chatStore.autoScroll - ? const SizedBox() + ? null : Container( padding: const EdgeInsets.symmetric(horizontal: 10.0), width: double.infinity, diff --git a/lib/screens/channel/chat/details/chat_users_list.dart b/lib/screens/channel/chat/details/chat_users_list.dart index 23c89ba3..56525ac9 100644 --- a/lib/screens/channel/chat/details/chat_users_list.dart +++ b/lib/screens/channel/chat/details/chat_users_list.dart @@ -142,12 +142,12 @@ class _ChattersListState extends State { ); }, ), - Observer(builder: (context) { - return AnimatedSwitcher( + Observer( + builder: (context) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: chatDetailStore.showJumpButton ? ScrollToTopButton(scrollController: _scrollController) : const SizedBox(), - ); - }), + child: chatDetailStore.showJumpButton ? ScrollToTopButton(scrollController: _scrollController) : null, + ), + ), ], ), ), diff --git a/lib/screens/channel/chat/widgets/chat_message.dart b/lib/screens/channel/chat/widgets/chat_message.dart index 8376b938..b680cb20 100644 --- a/lib/screens/channel/chat/widgets/chat_message.dart +++ b/lib/screens/channel/chat/widgets/chat_message.dart @@ -40,6 +40,7 @@ class ChatMessage extends StatelessWidget { assetsStore: assetsStore, useZeroWidth: settingsStore.showZeroWidth, useReadableColors: settingsStore.useReadableColors, + isLightTheme: Theme.of(context).brightness == Brightness.light, timestamp: timeStamps, ), ), @@ -75,6 +76,7 @@ class ChatMessage extends StatelessWidget { showMessage: settingsStore.showDeletedMessages, useZeroWidth: settingsStore.showZeroWidth, useReadableColors: settingsStore.useReadableColors, + isLightTheme: Theme.of(context).brightness == Brightness.light, timestamp: timeStamps, ), ), @@ -123,6 +125,7 @@ class ChatMessage extends StatelessWidget { assetsStore: assetsStore, useZeroWidth: settingsStore.showZeroWidth, useReadableColors: settingsStore.useReadableColors, + isLightTheme: Theme.of(context).brightness == Brightness.light, timestamp: timeStamps, ), ), diff --git a/lib/screens/channel/video/video.dart b/lib/screens/channel/video/video.dart index d0dbbeac..801e9f6c 100644 --- a/lib/screens/channel/video/video.dart +++ b/lib/screens/channel/video/video.dart @@ -27,7 +27,7 @@ class Video extends StatelessWidget { return Stack( children: [ WebView( - backgroundColor: const Color(0x00000000), + backgroundColor: Colors.black, initialUrl: 'https://player.twitch.tv/?channel=$userLogin&controls=false&muted=false&parent=frosty', javascriptMode: JavascriptMode.unrestricted, allowsInlineMediaPlayback: true, @@ -86,7 +86,7 @@ class Video extends StatelessWidget { ); } return WebView( - backgroundColor: const Color(0x00000000), + backgroundColor: Colors.black, initialUrl: 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty', javascriptMode: JavascriptMode.unrestricted, allowsInlineMediaPlayback: true, diff --git a/lib/screens/channel/video/video_overlay.dart b/lib/screens/channel/video/video_overlay.dart index 3e6a4085..80dc90df 100644 --- a/lib/screens/channel/video/video_overlay.dart +++ b/lib/screens/channel/video/video_overlay.dart @@ -25,7 +25,7 @@ class VideoOverlay extends StatelessWidget { tooltip: 'Back', icon: Icon( Icons.adaptive.arrow_back, - color: const Color(0xFFFFFFFF), + color: Colors.white, ), onPressed: Navigator.of(context).pop, ), @@ -36,7 +36,7 @@ class VideoOverlay extends StatelessWidget { tooltip: 'Settings', icon: const Icon( Icons.settings, - color: Color(0xFFFFFFFF), + color: Colors.white, ), onPressed: () => showModalBottomSheet( isScrollControlled: true, @@ -50,22 +50,35 @@ class VideoOverlay extends StatelessWidget { ), ), ), - Center( - child: IconButton( - tooltip: videoStore.paused ? 'Play' : 'Pause', - iconSize: 50.0, - icon: videoStore.paused - ? const Icon( - Icons.play_arrow, - color: Color(0xFFFFFFFF), - ) - : const Icon( - Icons.pause, - color: Color(0xFFFFFFFF), - ), - onPressed: videoStore.handlePausePlay, + if (Platform.isAndroid) + Center( + child: IconButton( + tooltip: videoStore.paused ? 'Play' : 'Pause', + iconSize: 80.0, + icon: videoStore.paused + ? const Icon( + Icons.play_arrow, + color: Colors.white, + ) + : const Icon( + Icons.pause, + color: Colors.white, + ), + onPressed: videoStore.handlePausePlay, + ), + ) + else if (!videoStore.paused) + Center( + child: IconButton( + tooltip: 'Pause', + iconSize: 80.0, + icon: const Icon( + Icons.pause, + color: Colors.white, + ), + onPressed: videoStore.handlePausePlay, + ), ), - ), Align( alignment: Alignment.bottomLeft, child: Row( @@ -85,7 +98,7 @@ class VideoOverlay extends StatelessWidget { Text( videoStore.streamInfo!.userName, style: const TextStyle( - color: Color(0xFFFFFFFF), + color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), @@ -100,7 +113,7 @@ class VideoOverlay extends StatelessWidget { maxLines: portrait ? 1 : 3, overflow: TextOverflow.ellipsis, style: const TextStyle( - color: Color(0xFFFFFFFF), + color: Colors.white, ), ), ), @@ -108,7 +121,7 @@ class VideoOverlay extends StatelessWidget { Text( '${videoStore.streamInfo?.gameName} for ${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers', style: const TextStyle( - color: Color(0xFFFFFFFF), + color: Colors.white, ), ), ], @@ -116,7 +129,7 @@ class VideoOverlay extends StatelessWidget { : Text( videoStore.streamInfo!.userName, style: const TextStyle( - color: Color(0xFFFFFFFF), + color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), @@ -129,7 +142,7 @@ class VideoOverlay extends StatelessWidget { tooltip: 'Refresh', icon: const Icon( Icons.refresh, - color: Color(0xFFFFFFFF), + color: Colors.white, ), onPressed: videoStore.handleRefresh, ), @@ -138,7 +151,7 @@ class VideoOverlay extends StatelessWidget { tooltip: 'Picture-in-Picture', icon: const Icon( Icons.picture_in_picture_alt_rounded, - color: Color(0xFFFFFFFF), + color: Colors.white, ), onPressed: videoStore.requestPictureInPicture, ), @@ -147,7 +160,7 @@ class VideoOverlay extends StatelessWidget { tooltip: 'Fullscreen', icon: const Icon( Icons.fullscreen, - color: Color(0xFFFFFFFF), + color: Colors.white, ), onPressed: () => videoStore.settingsStore.fullScreen = !videoStore.settingsStore.fullScreen, ) diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart index e8127b3e..3bbd0261 100644 --- a/lib/screens/channel/video_chat.dart +++ b/lib/screens/channel/video_chat.dart @@ -9,13 +9,11 @@ import 'package:frosty/screens/settings/settings.dart'; class VideoChat extends StatefulWidget { final String displayName; - final VideoStore videoStore; final ChatStore chatStore; const VideoChat({ Key? key, required this.displayName, - required this.videoStore, required this.chatStore, }) : super(key: key); @@ -24,23 +22,23 @@ class VideoChat extends StatefulWidget { } class _VideoChatState extends State { - final videoKey = GlobalKey(); - final chatKey = GlobalKey(); - @override Widget build(BuildContext context) { - final videoStore = widget.videoStore; final chatStore = widget.chatStore; final settingsStore = chatStore.settings; final video = Video( - key: videoKey, - userLogin: videoStore.userLogin, - videoStore: videoStore, + key: GlobalKey(), + userLogin: chatStore.channelName, + videoStore: VideoStore( + userLogin: chatStore.channelName, + authStore: chatStore.auth, + settingsStore: chatStore.settings, + ), ); final chat = Chat( - key: chatKey, + key: GlobalKey(), chatStore: chatStore, ); diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index 9d6d795d..d43f9c0a 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/models/channel.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; -import 'package:frosty/screens/channel/stores/video_store.dart'; import 'package:frosty/screens/channel/video_chat.dart'; import 'package:frosty/screens/home/stores/search_store.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; @@ -29,11 +28,6 @@ class SearchResultsChannels extends StatelessWidget { MaterialPageRoute( builder: (context) => VideoChat( displayName: channelInfo!.broadcasterName, - videoStore: VideoStore( - userLogin: channelInfo.broadcasterLogin, - authStore: searchStore.authStore, - settingsStore: context.read(), - ), chatStore: ChatStore( channelName: channelInfo.broadcasterLogin, auth: searchStore.authStore, @@ -98,11 +92,6 @@ class SearchResultsChannels extends StatelessWidget { MaterialPageRoute( builder: (context) => VideoChat( displayName: channel.displayName, - videoStore: VideoStore( - userLogin: channel.broadcasterLogin, - authStore: searchStore.authStore, - settingsStore: context.read(), - ), chatStore: ChatStore( channelName: channel.broadcasterLogin, auth: searchStore.authStore, diff --git a/lib/screens/home/top/categories.dart b/lib/screens/home/top/categories.dart index 87db79f3..98ff264d 100644 --- a/lib/screens/home/top/categories.dart +++ b/lib/screens/home/top/categories.dart @@ -49,7 +49,7 @@ class _CategoriesState extends State with AutomaticKeepAliveClientMi Observer( builder: (context) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : const SizedBox(), + child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : null, ), ), ], diff --git a/lib/screens/home/top/category_streams.dart b/lib/screens/home/top/category_streams.dart index aa8cd11a..cb77432e 100644 --- a/lib/screens/home/top/category_streams.dart +++ b/lib/screens/home/top/category_streams.dart @@ -83,7 +83,7 @@ class CategoryStreams extends StatelessWidget { child: Observer( builder: (context) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : const SizedBox(), + child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : null, ), ), ), diff --git a/lib/screens/home/widgets/stream_card.dart b/lib/screens/home/widgets/stream_card.dart index 01e21c30..d3b38da9 100644 --- a/lib/screens/home/widgets/stream_card.dart +++ b/lib/screens/home/widgets/stream_card.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; -import 'package:frosty/screens/channel/stores/video_store.dart'; import 'package:frosty/screens/channel/video_chat.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:frosty/widgets/profile_picture.dart'; @@ -34,11 +33,6 @@ class StreamCard extends StatelessWidget { MaterialPageRoute( builder: (context) => VideoChat( displayName: streamInfo.userName, - videoStore: VideoStore( - userLogin: streamInfo.userLogin, - authStore: context.read(), - settingsStore: context.read(), - ), chatStore: ChatStore( channelName: streamInfo.userLogin, auth: context.read(), @@ -63,7 +57,7 @@ class StreamCard extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 2.0), child: Text( DateTime.now().difference(DateTime.parse(streamInfo.startedAt)).toString().split('.')[0], - style: const TextStyle(fontSize: 12, color: Color(0xFFFFFFFF)), + style: const TextStyle(fontSize: 12, color: Colors.white), ), ) ], diff --git a/lib/screens/home/widgets/streams_list.dart b/lib/screens/home/widgets/streams_list.dart index 7f33f10d..1658acbb 100644 --- a/lib/screens/home/widgets/streams_list.dart +++ b/lib/screens/home/widgets/streams_list.dart @@ -55,7 +55,7 @@ class _StreamsListState extends State with AutomaticKeepAliveClient Observer( builder: (context) => AnimatedSwitcher( duration: const Duration(milliseconds: 200), - child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : const SizedBox(), + child: store.showJumpButton ? ScrollToTopButton(scrollController: store.scrollController) : null, ), ), ], diff --git a/lib/screens/settings/chat_settings.dart b/lib/screens/settings/chat_settings.dart index 6c2b919e..ad4f3568 100644 --- a/lib/screens/settings/chat_settings.dart +++ b/lib/screens/settings/chat_settings.dart @@ -53,7 +53,7 @@ class _ChatSettingsState extends State { SwitchListTile.adaptive( isThreeLine: true, title: const Text('Use readable colors for chat names'), - subtitle: const Text('Makes dark names in chat readable by boosting their lightness value.'), + subtitle: const Text('Adjusts the lightness value of overly bright/dark names in chat.'), value: settingsStore.useReadableColors, onChanged: (newValue) => settingsStore.useReadableColors = newValue, ), diff --git a/lib/screens/settings/general_settings.dart b/lib/screens/settings/general_settings.dart index fbb4f77d..cbddc995 100644 --- a/lib/screens/settings/general_settings.dart +++ b/lib/screens/settings/general_settings.dart @@ -10,17 +10,26 @@ class GeneralSettings extends StatelessWidget { @override Widget build(BuildContext context) { + const themes = ['System', 'Light', 'Dark', 'Black']; + return Observer( builder: (context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SectionHeader('General'), - SwitchListTile.adaptive( - title: const Text('Use OLED theme'), - subtitle: const Text('An all-black theme for OLED screens.'), - value: settingsStore.useOledTheme, - onChanged: (newValue) => settingsStore.useOledTheme = newValue, + ListTile( + title: const Text('Theme'), + trailing: DropdownButton( + value: settingsStore.themeType, + onChanged: (ThemeType? newTheme) => settingsStore.themeType = newTheme!, + items: ThemeType.values + .map((ThemeType value) => DropdownMenuItem( + value: value, + child: Text(themes[value.index]), + )) + .toList(), + ), ), SwitchListTile.adaptive( title: const Text('Show stream uptime on thumbnails'), diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart index c69990fb..387a28e0 100644 --- a/lib/screens/settings/stores/settings_store.dart +++ b/lib/screens/settings/stores/settings_store.dart @@ -14,9 +14,9 @@ class SettingsStore extends _SettingsStoreBase with _$SettingsStore { abstract class _SettingsStoreBase with Store { //General Settings - @JsonKey(defaultValue: false) + @JsonKey(defaultValue: ThemeType.system) @observable - var useOledTheme = false; + var themeType = ThemeType.system; @JsonKey(defaultValue: false) @observable @@ -90,3 +90,10 @@ abstract class _SettingsStoreBase with Store { ); } } + +enum ThemeType { + system, + light, + dark, + black, +} diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart index c55dafa1..14c446f9 100644 --- a/lib/screens/settings/stores/settings_store.g.dart +++ b/lib/screens/settings/stores/settings_store.g.dart @@ -8,7 +8,9 @@ part of 'settings_store.dart'; SettingsStore _$SettingsStoreFromJson(Map json) => SettingsStore() - ..useOledTheme = json['useOledTheme'] as bool? ?? false + ..themeType = + $enumDecodeNullable(_$ThemeTypeEnumMap, json['themeType']) ?? + ThemeType.system ..showThumbnailUptime = json['showThumbnailUptime'] as bool? ?? false ..showVideo = json['showVideo'] as bool? ?? true ..showOverlay = json['showOverlay'] as bool? ?? true @@ -27,7 +29,7 @@ SettingsStore _$SettingsStoreFromJson(Map json) => Map _$SettingsStoreToJson(SettingsStore instance) => { - 'useOledTheme': instance.useOledTheme, + 'themeType': _$ThemeTypeEnumMap[instance.themeType], 'showThumbnailUptime': instance.showThumbnailUptime, 'showVideo': instance.showVideo, 'showOverlay': instance.showOverlay, @@ -44,6 +46,13 @@ Map _$SettingsStoreToJson(SettingsStore instance) => 'expandInfo': instance.expandInfo, }; +const _$ThemeTypeEnumMap = { + ThemeType.system: 'system', + ThemeType.light: 'light', + ThemeType.dark: 'dark', + ThemeType.black: 'black', +}; + // ************************************************************************** // StoreGenerator // ************************************************************************** @@ -51,18 +60,18 @@ Map _$SettingsStoreToJson(SettingsStore instance) => // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic mixin _$SettingsStore on _SettingsStoreBase, Store { - final _$useOledThemeAtom = Atom(name: '_SettingsStoreBase.useOledTheme'); + final _$themeTypeAtom = Atom(name: '_SettingsStoreBase.themeType'); @override - bool get useOledTheme { - _$useOledThemeAtom.reportRead(); - return super.useOledTheme; + ThemeType get themeType { + _$themeTypeAtom.reportRead(); + return super.themeType; } @override - set useOledTheme(bool value) { - _$useOledThemeAtom.reportWrite(value, super.useOledTheme, () { - super.useOledTheme = value; + set themeType(ThemeType value) { + _$themeTypeAtom.reportWrite(value, super.themeType, () { + super.themeType = value; }); } @@ -286,7 +295,7 @@ mixin _$SettingsStore on _SettingsStoreBase, Store { @override String toString() { return ''' -useOledTheme: ${useOledTheme}, +themeType: ${themeType}, showThumbnailUptime: ${showThumbnailUptime}, showVideo: ${showVideo}, showOverlay: ${showOverlay}, diff --git a/lib/widgets/profile_picture.dart b/lib/widgets/profile_picture.dart index 72a34579..d9e52bde 100644 --- a/lib/widgets/profile_picture.dart +++ b/lib/widgets/profile_picture.dart @@ -22,6 +22,7 @@ class ProfilePicture extends StatelessWidget { builder: (context, AsyncSnapshot snapshot) { return CircleAvatar( radius: radius, + backgroundColor: Colors.transparent, foregroundImage: snapshot.hasData && snapshot.data != null ? CachedNetworkImageProvider(snapshot.data!.profileImageUrl) : null, ); },