Theme improvements & fixes

- Added new light theme
- Added readable colors for light theme
- Changed theme settings to use dropdown button
- Fixed state disposing prematurely when rotating
This commit is contained in:
Tommy Chow 2022-01-11 00:45:43 -05:00
parent a7b9d3a30d
commit 5bc4217f32
18 changed files with 144 additions and 84 deletions

View file

@ -57,7 +57,33 @@ class MyApp extends StatelessWidget {
final authStore = context.read<AuthStore>();
final settingsStore = context.read<SettingsStore>();
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(

View file

@ -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.

View file

@ -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,

View file

@ -142,12 +142,12 @@ class _ChattersListState extends State<ChattersList> {
);
},
),
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,
),
),
],
),
),

View file

@ -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,
),
),

View file

@ -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,

View file

@ -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,
)

View file

@ -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<VideoChat> {
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,
);

View file

@ -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<SettingsStore>(),
),
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<SettingsStore>(),
),
chatStore: ChatStore(
channelName: channel.broadcasterLogin,
auth: searchStore.authStore,

View file

@ -49,7 +49,7 @@ class _CategoriesState extends State<Categories> 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,
),
),
],

View file

@ -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,
),
),
),

View file

@ -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<AuthStore>(),
settingsStore: context.read<SettingsStore>(),
),
chatStore: ChatStore(
channelName: streamInfo.userLogin,
auth: context.read<AuthStore>(),
@ -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),
),
)
],

View file

@ -55,7 +55,7 @@ class _StreamsListState extends State<StreamsList> 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,
),
),
],

View file

@ -53,7 +53,7 @@ class _ChatSettingsState extends State<ChatSettings> {
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,
),

View file

@ -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'),

View file

@ -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,
}

View file

@ -8,7 +8,9 @@ part of 'settings_store.dart';
SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> 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<String, dynamic> json) =>
Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
<String, dynamic>{
'useOledTheme': instance.useOledTheme,
'themeType': _$ThemeTypeEnumMap[instance.themeType],
'showThumbnailUptime': instance.showThumbnailUptime,
'showVideo': instance.showVideo,
'showOverlay': instance.showOverlay,
@ -44,6 +46,13 @@ Map<String, dynamic> _$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<String, dynamic> _$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},

View file

@ -22,6 +22,7 @@ class ProfilePicture extends StatelessWidget {
builder: (context, AsyncSnapshot<UserTwitch?> snapshot) {
return CircleAvatar(
radius: radius,
backgroundColor: Colors.transparent,
foregroundImage: snapshot.hasData && snapshot.data != null ? CachedNetworkImageProvider(snapshot.data!.profileImageUrl) : null,
);
},