Improve design and usability, general tweaks and fixes (#226)
See #226 for more details.
This commit is contained in:
parent
90841e0589
commit
7881ce22b4
69 changed files with 3040 additions and 2812 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
|
|
@ -2,7 +2,6 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:device_preview/device_preview.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
|
@ -61,7 +60,7 @@ void main() async {
|
|||
|
||||
runApp(
|
||||
DevicePreview(
|
||||
enabled: !kReleaseMode,
|
||||
enabled: false,
|
||||
builder: (context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
|
|
@ -83,25 +82,44 @@ void main() async {
|
|||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
const gray = Color.fromRGBO(18, 18, 18, 1.0);
|
||||
const lightGray = Color.fromRGBO(28, 28, 28, 1.0);
|
||||
const purple = Color(0xff9146ff);
|
||||
|
||||
const inputTheme = InputDecorationTheme(
|
||||
filled: true,
|
||||
contentPadding: EdgeInsets.all(10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30.0)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(style: BorderStyle.none),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30.0)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(style: BorderStyle.none),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30.0)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
borderSide: BorderSide(style: BorderStyle.none),
|
||||
),
|
||||
);
|
||||
|
||||
const tooltipTheme = TooltipThemeData(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
margin: EdgeInsets.symmetric(horizontal: 5.0),
|
||||
decoration: BoxDecoration(
|
||||
color: lightGray,
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
textStyle: TextStyle(color: Colors.white),
|
||||
);
|
||||
|
||||
const snackBarTheme = SnackBarThemeData(
|
||||
backgroundColor: lightGray,
|
||||
contentTextStyle: TextStyle(color: Colors.white),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
);
|
||||
|
||||
final lightTheme = ThemeData(
|
||||
canvasColor: Colors.white,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
|
|
@ -127,6 +145,8 @@ final lightTheme = ThemeData(
|
|||
unselectedLabelColor: Colors.grey,
|
||||
),
|
||||
inputDecorationTheme: inputTheme,
|
||||
tooltipTheme: tooltipTheme,
|
||||
snackBarTheme: snackBarTheme,
|
||||
);
|
||||
|
||||
final darkTheme = ThemeData(
|
||||
|
|
@ -152,6 +172,8 @@ final darkTheme = ThemeData(
|
|||
dialogBackgroundColor: gray,
|
||||
toggleableActiveColor: purple,
|
||||
inputDecorationTheme: inputTheme,
|
||||
tooltipTheme: tooltipTheme,
|
||||
snackBarTheme: snackBarTheme,
|
||||
);
|
||||
|
||||
final oledTheme = ThemeData(
|
||||
|
|
@ -175,6 +197,8 @@ final oledTheme = ThemeData(
|
|||
dialogBackgroundColor: Colors.black,
|
||||
toggleableActiveColor: purple,
|
||||
inputDecorationTheme: inputTheme,
|
||||
tooltipTheme: tooltipTheme,
|
||||
snackBarTheme: snackBarTheme,
|
||||
);
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
|
|
@ -191,7 +215,9 @@ class MyApp extends StatelessWidget {
|
|||
locale: DevicePreview.locale(context),
|
||||
title: 'Frosty',
|
||||
theme: lightTheme,
|
||||
darkTheme: settingsStore.themeType == ThemeType.dark || settingsStore.themeType == ThemeType.system ? darkTheme : oledTheme,
|
||||
darkTheme: settingsStore.themeType == ThemeType.dark || settingsStore.themeType == ThemeType.system
|
||||
? darkTheme
|
||||
: oledTheme,
|
||||
themeMode: settingsStore.themeType == ThemeType.system
|
||||
? ThemeMode.system
|
||||
: settingsStore.themeType == ThemeType.light
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -8,6 +7,7 @@ import 'package:frosty/models/badges.dart';
|
|||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ class IRCMessage {
|
|||
required double emoteScale,
|
||||
required bool isLightTheme,
|
||||
required bool launchExternal,
|
||||
required void Function()? onLongPressName,
|
||||
required void Function()? onTapName,
|
||||
bool showMessage = true,
|
||||
bool useReadableColors = false,
|
||||
TimestampType timestamp = TimestampType.disabled,
|
||||
|
|
@ -174,7 +174,8 @@ class IRCMessage {
|
|||
badgeUrl = botBadge.url;
|
||||
skipBot = true;
|
||||
} else if (ffzRoomInfo?.modUrls != null) {
|
||||
badgeUrl = 'https:${ffzRoomInfo!.modUrls?.url4x ?? ffzRoomInfo.modUrls?.url2x ?? ffzRoomInfo.modUrls!.url1x}';
|
||||
badgeUrl =
|
||||
'https:${ffzRoomInfo!.modUrls?.url4x ?? ffzRoomInfo.modUrls?.url2x ?? ffzRoomInfo.modUrls!.url1x}';
|
||||
}
|
||||
|
||||
final newBadge = Badge(
|
||||
|
|
@ -196,7 +197,8 @@ class IRCMessage {
|
|||
|
||||
// Add custom FFZ vip badge if it exists
|
||||
if (badgeInfo.name == 'VIP' && ffzRoomInfo?.vipBadge != null) {
|
||||
badgeUrl = 'https:${ffzRoomInfo!.vipBadge?.url4x ?? ffzRoomInfo.vipBadge?.url2x ?? ffzRoomInfo.vipBadge!.url1x}';
|
||||
badgeUrl =
|
||||
'https:${ffzRoomInfo!.vipBadge?.url4x ?? ffzRoomInfo.vipBadge?.url2x ?? ffzRoomInfo.vipBadge!.url1x}';
|
||||
}
|
||||
|
||||
final newBadge = Badge(
|
||||
|
|
@ -285,14 +287,15 @@ class IRCMessage {
|
|||
}
|
||||
|
||||
// Add the display name (username) to the span and apply the onLongPressName callback.
|
||||
final displayName = tags['display-name']!;
|
||||
span.add(
|
||||
TextSpan(
|
||||
text: tags['display-name']!,
|
||||
text: regexEnglish.hasMatch(displayName) ? displayName : '$displayName ($user)',
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
recognizer: LongPressGestureRecognizer()..onLongPress = onLongPressName,
|
||||
recognizer: TapGestureRecognizer()..onTap = onTapName,
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -351,11 +354,11 @@ class IRCMessage {
|
|||
style: textStyle?.copyWith(fontSize: emoteSize - 5),
|
||||
),
|
||||
...emoteStack.reversed.map(
|
||||
(emote) => CachedNetworkImage(
|
||||
(emote) => FrostyCachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: emote.height != null ? emote.height! * emoteScale : emoteSize,
|
||||
width: emote.width != null ? emote.width! * emoteScale : emoteSize,
|
||||
useFade: false,
|
||||
),
|
||||
)
|
||||
];
|
||||
|
|
@ -366,43 +369,44 @@ class IRCMessage {
|
|||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
richMessage: WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
if (nextWordIsEmoji)
|
||||
Text(
|
||||
words[index],
|
||||
style: textStyle?.copyWith(fontSize: 75),
|
||||
),
|
||||
...emoteStack.reversed.map(
|
||||
(emote) => CachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: 80,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Column(
|
||||
children: [
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
if (nextWordIsEmoji)
|
||||
Text(
|
||||
nextWordIsEmoji ? '${words[index]} - Emoji' : '${emoteStack.last.name} - ${emoteType[emoteStack.last.type.index]}',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
words[index],
|
||||
style: textStyle?.copyWith(fontSize: 75),
|
||||
),
|
||||
Text(
|
||||
nextWordIsEmoji ? message.join(', ') : message.skip(1).join(', '),
|
||||
style: const TextStyle(color: Colors.black),
|
||||
...emoteStack.reversed.map(
|
||||
(emote) => FrostyCachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
height: 80,
|
||||
useFade: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
nextWordIsEmoji
|
||||
? '${words[index]} - Emoji'
|
||||
: '${emoteStack.last.name} - ${emoteType[emoteStack.last.type.index]}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
nextWordIsEmoji ? message.join(', ') : message.skip(1).join(', '),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
preferBelow: false,
|
||||
|
|
@ -470,11 +474,11 @@ class IRCMessage {
|
|||
if (backgroundColor != null) {
|
||||
return ColoredBox(
|
||||
color: backgroundColor,
|
||||
child: CachedNetworkImage(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: badge.url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: size,
|
||||
width: size,
|
||||
useFade: false,
|
||||
),
|
||||
);
|
||||
} else if (isSvg == true) {
|
||||
|
|
@ -484,11 +488,11 @@ class IRCMessage {
|
|||
width: size,
|
||||
);
|
||||
} else {
|
||||
return CachedNetworkImage(
|
||||
return FrostyCachedNetworkImage(
|
||||
imageUrl: badge.url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: size,
|
||||
width: size,
|
||||
useFade: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -502,24 +506,23 @@ class IRCMessage {
|
|||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
richMessage: WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_createBadgeWidget(
|
||||
badge: badge,
|
||||
size: 80,
|
||||
backgroundColor: backgroundColor,
|
||||
isSvg: isSvg,
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
badge.name,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_createBadgeWidget(
|
||||
badge: badge,
|
||||
size: 80,
|
||||
backgroundColor: backgroundColor,
|
||||
isSvg: isSvg,
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
badge.name,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
preferBelow: false,
|
||||
|
|
@ -541,36 +544,35 @@ class IRCMessage {
|
|||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Tooltip(
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
richMessage: WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: 80,
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
emote.name,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
Text(
|
||||
emoteType[emote.type.index],
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
FrostyCachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
height: 80,
|
||||
useFade: false,
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
emote.name,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
emoteType[emote.type.index],
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
preferBelow: false,
|
||||
child: CachedNetworkImage(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: emote.url,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
fadeInDuration: const Duration(),
|
||||
height: height,
|
||||
width: width,
|
||||
useFade: false,
|
||||
placeholder: (context, url) => const SizedBox(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -584,7 +586,8 @@ class IRCMessage {
|
|||
text: text,
|
||||
style: style?.copyWith(color: Colors.blue),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => launchUrl(Uri.parse(text), mode: launchExternal ? LaunchMode.externalApplication : LaunchMode.inAppWebView),
|
||||
..onTap = () => launchUrl(Uri.parse(text),
|
||||
mode: launchExternal ? LaunchMode.externalApplication : LaunchMode.inAppWebView),
|
||||
);
|
||||
} else {
|
||||
return TextSpan(text: text, style: style);
|
||||
|
|
@ -633,7 +636,8 @@ class IRCMessage {
|
|||
|
||||
// If the username exists, set it.
|
||||
// tmi.twitch.tv means the message was sent by Twitch rather than a user, so will be irrelevant.
|
||||
final String? user = splitMessage[0] == 'tmi.twitch.tv' ? null : splitMessage[0].substring(0, splitMessage[0].indexOf('!'));
|
||||
final String? user =
|
||||
splitMessage[0] == 'tmi.twitch.tv' ? null : splitMessage[0].substring(0, splitMessage[0].indexOf('!'));
|
||||
|
||||
// If there is an associated message, set it.
|
||||
//
|
||||
|
|
@ -697,7 +701,11 @@ class IRCMessage {
|
|||
if (userLogin != null) mention = message.toLowerCase().contains(userLogin);
|
||||
|
||||
// Escape the message
|
||||
message = message.split(' ').map((word) => word.replaceAll('\u{E0000}', '').trim()).where((element) => element != '').join(' ');
|
||||
message = message
|
||||
.split(' ')
|
||||
.map((word) => word.replaceAll('\u{E0000}', '').trim())
|
||||
.where((element) => element != '')
|
||||
.join(' ');
|
||||
|
||||
final emojiBuffer = StringBuffer();
|
||||
final wordBuffer = StringBuffer();
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
|
|||
import 'package:frosty/screens/channel/chat/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/channel/video/video.dart';
|
||||
import 'package:frosty/screens/channel/video/video_bar.dart';
|
||||
import 'package:frosty/screens/channel/video/video_overlay.dart';
|
||||
import 'package:frosty/screens/channel/video/video_store.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/widgets/modal.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
import 'package:frosty/widgets/notification.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simple_pip_mode/actions/pip_actions_layout.dart';
|
||||
import 'package:simple_pip_mode/pip_widget.dart';
|
||||
|
|
@ -72,32 +73,16 @@ class _VideoChatState extends State<VideoChat> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
|
||||
final settingsStore = _chatStore.settings;
|
||||
|
||||
void showSettings() => showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => FrostyModal(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Settings(settingsStore: settingsStore),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final appBar = AppBar(
|
||||
final appBar = FrostyAppBar(
|
||||
title: Text(
|
||||
regexEnglish.hasMatch(_chatStore.displayName) ? _chatStore.displayName : '${_chatStore.displayName} (${_chatStore.channelName})',
|
||||
style: const TextStyle(fontSize: 20),
|
||||
regexEnglish.hasMatch(_chatStore.displayName)
|
||||
? _chatStore.displayName
|
||||
: '${_chatStore.displayName} (${_chatStore.channelName})',
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: showSettings,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final player = GestureDetector(
|
||||
|
|
@ -108,14 +93,10 @@ class _VideoChatState extends State<VideoChat> {
|
|||
),
|
||||
);
|
||||
|
||||
final videoOverlay = VideoOverlay(
|
||||
videoStore: _videoStore,
|
||||
onSettingsPressed: showSettings,
|
||||
);
|
||||
|
||||
final overlay = GestureDetector(
|
||||
onLongPress: _videoStore.handleToggleOverlay,
|
||||
onDoubleTap: MediaQuery.of(context).orientation == Orientation.landscape ? () => settingsStore.fullScreen = !settingsStore.fullScreen : null,
|
||||
onDoubleTap:
|
||||
orientation == Orientation.landscape ? () => settingsStore.fullScreen = !settingsStore.fullScreen : null,
|
||||
onTap: () {
|
||||
if (_chatStore.assetsStore.showEmoteMenu) {
|
||||
_chatStore.assetsStore.showEmoteMenu = false;
|
||||
|
|
@ -129,24 +110,20 @@ class _VideoChatState extends State<VideoChat> {
|
|||
},
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final videoOverlay = VideoOverlay(
|
||||
videoStore: _videoStore,
|
||||
chatStore: _chatStore,
|
||||
);
|
||||
|
||||
if (_videoStore.paused || _videoStore.streamInfo == null) return videoOverlay;
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: _videoStore.overlayVisible ? 1.0 : 0.0,
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: const [0.0, 0.25, 0.75, 1.0],
|
||||
colors: [
|
||||
Colors.black.withOpacity(settingsStore.overlayOpacity),
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(settingsStore.overlayOpacity),
|
||||
],
|
||||
),
|
||||
color: Colors.black.withOpacity(settingsStore.overlayOpacity),
|
||||
),
|
||||
child: IgnorePointer(
|
||||
ignoring: !_videoStore.overlayVisible,
|
||||
|
|
@ -171,87 +148,147 @@ class _VideoChatState extends State<VideoChat> {
|
|||
},
|
||||
);
|
||||
|
||||
final chat = Chat(
|
||||
key: _chatKey,
|
||||
chatStore: _chatStore,
|
||||
final chat = Observer(
|
||||
builder: (context) {
|
||||
final videoBarVisible = _videoStore.streamInfo != null &&
|
||||
_chatStore.settings.showVideo &&
|
||||
(_videoStore.paused || _videoStore.overlayVisible);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Chat(
|
||||
key: _chatKey,
|
||||
chatStore: _chatStore,
|
||||
),
|
||||
if (orientation == Orientation.portrait)
|
||||
AnimatedOpacity(
|
||||
opacity: videoBarVisible ? 1 : 0,
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: IgnorePointer(
|
||||
ignoring: !videoBarVisible,
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_videoStore.streamInfo != null)
|
||||
VideoBar(
|
||||
streamInfo: _videoStore.streamInfo!,
|
||||
tappableCategory: false,
|
||||
),
|
||||
const Divider(height: 1, thickness: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: _chatStore.notification != null
|
||||
? Align(
|
||||
alignment:
|
||||
_chatStore.settings.chatNotificationsOnBottom ? Alignment.bottomCenter : Alignment.topCenter,
|
||||
child: FrostyNotification(
|
||||
message: _chatStore.notification!,
|
||||
showPasteButton: _chatStore.notification!.contains('copied'),
|
||||
onButtonPressed: () async {
|
||||
// Paste clipboard text into the text controller.
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
|
||||
if (data != null) _chatStore.textController.text = data.text!;
|
||||
|
||||
_chatStore.updateNotification('');
|
||||
},
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final videoChat = Scaffold(
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
if (orientation == Orientation.landscape && !settingsStore.landscapeForceVerticalChat) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
body: Observer(
|
||||
builder: (context) {
|
||||
if (orientation == Orientation.landscape && !settingsStore.landscapeForceVerticalChat) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
|
||||
final landscapeChat = AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: _chatStore.expandChat ? MediaQuery.of(context).size.width / 2 : MediaQuery.of(context).size.width * _chatStore.settings.chatWidth,
|
||||
curve: Curves.ease,
|
||||
color: _chatStore.settings.fullScreen
|
||||
? Colors.black.withOpacity(_chatStore.settings.fullScreenChatOverlayOpacity)
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: chat,
|
||||
);
|
||||
final landscapeChat = AnimatedContainer(
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: _chatStore.expandChat
|
||||
? MediaQuery.of(context).size.width / 2
|
||||
: MediaQuery.of(context).size.width * _chatStore.settings.chatWidth,
|
||||
color: _chatStore.settings.fullScreen
|
||||
? Colors.black.withOpacity(_chatStore.settings.fullScreenChatOverlayOpacity)
|
||||
: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: chat,
|
||||
);
|
||||
|
||||
final overlayChat = Visibility(
|
||||
visible: settingsStore.fullScreenChatOverlay,
|
||||
maintainState: true,
|
||||
child: Theme(
|
||||
data: darkTheme,
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(color: Colors.white),
|
||||
child: landscapeChat,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return ColoredBox(
|
||||
color: settingsStore.showVideo ? Colors.black : Theme.of(context).scaffoldBackgroundColor,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
left:
|
||||
(settingsStore.landscapeCutout == LandscapeCutoutType.both || settingsStore.landscapeCutout == LandscapeCutoutType.left) ? false : true,
|
||||
right: (settingsStore.landscapeCutout == LandscapeCutoutType.both || settingsStore.landscapeCutout == LandscapeCutoutType.right)
|
||||
? false
|
||||
: true,
|
||||
child: settingsStore.showVideo
|
||||
? settingsStore.fullScreen
|
||||
? Stack(
|
||||
children: [
|
||||
player,
|
||||
if (settingsStore.showOverlay)
|
||||
Row(
|
||||
children: settingsStore.landscapeChatLeftSide
|
||||
? [overlayChat, Expanded(child: overlay)]
|
||||
: [Expanded(child: overlay), overlayChat],
|
||||
)
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children:
|
||||
settingsStore.landscapeChatLeftSide ? [landscapeChat, Expanded(child: video)] : [Expanded(child: video), landscapeChat],
|
||||
)
|
||||
: Column(
|
||||
children: [appBar, Expanded(child: chat)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
if (!settingsStore.showVideo) appBar else AspectRatio(aspectRatio: 16 / 9, child: video),
|
||||
Expanded(child: chat),
|
||||
],
|
||||
final overlayChat = Visibility(
|
||||
visible: settingsStore.fullScreenChatOverlay,
|
||||
maintainState: true,
|
||||
child: Theme(
|
||||
data: darkTheme,
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(color: Colors.white),
|
||||
child: landscapeChat,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return ColoredBox(
|
||||
color: settingsStore.showVideo ? Colors.black : Theme.of(context).scaffoldBackgroundColor,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
left: (settingsStore.landscapeCutout == LandscapeCutoutType.both ||
|
||||
settingsStore.landscapeCutout == LandscapeCutoutType.left)
|
||||
? false
|
||||
: true,
|
||||
right: (settingsStore.landscapeCutout == LandscapeCutoutType.both ||
|
||||
settingsStore.landscapeCutout == LandscapeCutoutType.right)
|
||||
? false
|
||||
: true,
|
||||
child: settingsStore.showVideo
|
||||
? settingsStore.fullScreen
|
||||
? Stack(
|
||||
children: [
|
||||
player,
|
||||
if (settingsStore.showOverlay)
|
||||
Row(
|
||||
children: settingsStore.landscapeChatLeftSide
|
||||
? [overlayChat, Expanded(child: overlay)]
|
||||
: [Expanded(child: overlay), overlayChat],
|
||||
)
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: settingsStore.landscapeChatLeftSide
|
||||
? [landscapeChat, Expanded(child: video)]
|
||||
: [Expanded(child: video), landscapeChat],
|
||||
)
|
||||
: Column(
|
||||
children: [appBar, Expanded(child: chat)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
if (!settingsStore.showVideo) appBar else AspectRatio(aspectRatio: 16 / 9, child: video),
|
||||
Expanded(child: chat),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_panel.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/recent_emotes_panel.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/notification.dart';
|
||||
import 'package:frosty/widgets/page_view.dart';
|
||||
|
||||
class Chat extends StatelessWidget {
|
||||
final ChatStore chatStore;
|
||||
|
|
@ -52,39 +54,19 @@ class Chat extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: chatStore.notification != null
|
||||
? Align(
|
||||
alignment: chatStore.settings.chatNotificationsOnBottom ? Alignment.bottomCenter : Alignment.topCenter,
|
||||
child: FrostyNotification(
|
||||
message: chatStore.notification!,
|
||||
showPasteButton: chatStore.notification!.contains('copied'),
|
||||
onButtonPressed: () async {
|
||||
// Paste clipboard text into the text controller.
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
|
||||
if (data != null) chatStore.textController.text = data.text!;
|
||||
|
||||
chatStore.updateNotification('');
|
||||
},
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: chatStore.autoScroll
|
||||
? null
|
||||
: Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
|
||||
onPressed: chatStore.resumeScroll,
|
||||
icon: const Icon(Icons.arrow_downward),
|
||||
icon: const Icon(Icons.keyboard_double_arrow_down_rounded),
|
||||
child: const Text('Resume scroll'),
|
||||
),
|
||||
),
|
||||
|
|
@ -95,15 +77,60 @@ class Chat extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore),
|
||||
AnimatedContainer(
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: chatStore.assetsStore.showEmoteMenu ? MediaQuery.of(context).size.height / 3 : 0,
|
||||
child: AnimatedOpacity(
|
||||
WillPopScope(
|
||||
onWillPop: Platform.isAndroid
|
||||
? () async {
|
||||
// If pressing the back button on Android while the emote menu is open, close it instead of going back to the streams list.
|
||||
if (chatStore.assetsStore.showEmoteMenu) {
|
||||
chatStore.assetsStore.showEmoteMenu = false;
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: AnimatedContainer(
|
||||
curve: Curves.ease,
|
||||
opacity: chatStore.assetsStore.showEmoteMenu ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: EmoteMenu(chatStore: chatStore),
|
||||
height: chatStore.assetsStore.showEmoteMenu
|
||||
? MediaQuery.of(context).size.height /
|
||||
(MediaQuery.of(context).orientation == Orientation.portrait ? 3 : 2)
|
||||
: 0,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: chatStore.assetsStore.showEmoteMenu
|
||||
? FrostyPageView(
|
||||
headers: const [
|
||||
'Recent',
|
||||
'Twitch',
|
||||
'7TV',
|
||||
'BTTV',
|
||||
'FFZ',
|
||||
],
|
||||
children: [
|
||||
RecentEmotesPanel(
|
||||
chatStore: chatStore,
|
||||
),
|
||||
EmoteMenuPanel(
|
||||
chatStore: chatStore,
|
||||
twitchEmotes: chatStore.assetsStore.userEmoteSectionToEmotes,
|
||||
),
|
||||
...[
|
||||
chatStore.assetsStore.sevenTVEmotes,
|
||||
chatStore.assetsStore.bttvEmotes,
|
||||
chatStore.assetsStore.ffzEmotes
|
||||
].map(
|
||||
(emotes) => EmoteMenuPanel(
|
||||
chatStore: chatStore,
|
||||
emotes: emotes,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_details_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_modes.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_users_list.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/modal.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:frosty/screens/settings/settings.dart';
|
||||
import 'package:frosty/widgets/bottom_sheet.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
|
||||
class ChatDetails extends StatefulWidget {
|
||||
final ChatDetailsStore chatDetailsStore;
|
||||
|
|
@ -23,6 +30,109 @@ class ChatDetails extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ChatDetailsState extends State<ChatDetails> {
|
||||
Future<void> _showSleepTimerDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Sleep Timer',
|
||||
content: Observer(
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: widget.chatStore.sleepTimer != null && widget.chatStore.sleepTimer!.isActive ? 1.0 : 0.5,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.timer_rounded),
|
||||
Text(
|
||||
' ${widget.chatStore.timeRemaining.toString().split('.')[0]}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontFeatures: [FontFeature.tabularFigures()]),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'Cancel sleep timer',
|
||||
onPressed: widget.chatStore.cancelSleepTimer,
|
||||
icon: const Icon(Icons.cancel_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
DropdownButton(
|
||||
value: widget.chatStore.sleepHours,
|
||||
items: List.generate(24, (index) => index)
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e.toString())))
|
||||
.toList(),
|
||||
onChanged: (int? hours) => widget.chatStore.sleepHours = hours!,
|
||||
menuMaxHeight: 200,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
const Text('Hours'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
DropdownButton(
|
||||
value: widget.chatStore.sleepMinutes,
|
||||
items: List.generate(60, (index) => index)
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e.toString())))
|
||||
.toList(),
|
||||
onChanged: (int? minutes) => widget.chatStore.sleepMinutes = minutes!,
|
||||
menuMaxHeight: 200,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
const Text('Minutes'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Observer(
|
||||
builder: (context) => Button(
|
||||
onPressed: widget.chatStore.sleepHours == 0 && widget.chatStore.sleepMinutes == 0
|
||||
? null
|
||||
: () => widget.chatStore.updateSleepTimer(
|
||||
onTimerFinished: () => navigatorKey.currentState?.popUntil((route) => route.isFirst),
|
||||
),
|
||||
child: const Text('Set timer'),
|
||||
),
|
||||
),
|
||||
Button(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.grey,
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showClearDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Clear Recent Emotes',
|
||||
message: 'Are you sure you want to clear your recent emotes?',
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
setState(widget.chatStore.assetsStore.recentEmotes.clear);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.grey,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -31,57 +141,81 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FrostyModal(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionHeader('Chat modes'),
|
||||
ListTile(
|
||||
title: ChatModes(roomState: widget.chatDetailsStore.roomState),
|
||||
),
|
||||
const SectionHeader('Other'),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.people),
|
||||
title: const Text('Chatters'),
|
||||
onTap: () => showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => FrostyModal(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore: widget.chatDetailsStore,
|
||||
chatStore: widget.chatStore,
|
||||
userLogin: widget.userLogin,
|
||||
),
|
||||
),
|
||||
final children = [
|
||||
ListTile(
|
||||
title: ChatModes(roomState: widget.chatDetailsStore.roomState),
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.people_outline),
|
||||
title: 'Chatters',
|
||||
onTap: () => showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => FrostyBottomSheet(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore: widget.chatDetailsStore,
|
||||
chatStore: widget.chatStore,
|
||||
userLogin: widget.userLogin,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.refresh),
|
||||
title: const Text('Reconnect to chat'),
|
||||
onTap: () {
|
||||
widget.chatStore.updateNotification('Reconnecting to chat...');
|
||||
|
||||
widget.chatStore.connectToChat();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.refresh),
|
||||
title: const Text('Refresh badges and emotes'),
|
||||
onTap: () async {
|
||||
await widget.chatStore.getAssets();
|
||||
|
||||
widget.chatStore.updateNotification('Badges and emotes refreshed');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.timer_outlined),
|
||||
title: 'Sleep timer',
|
||||
onTap: () => _showSleepTimerDialog(context),
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.delete_outline_rounded),
|
||||
title: 'Clear recent emotes',
|
||||
onTap: _showClearDialog,
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.refresh_rounded),
|
||||
title: 'Reconnect to chat',
|
||||
onTap: () {
|
||||
widget.chatStore.updateNotification('Reconnecting to chat...');
|
||||
|
||||
widget.chatStore.connectToChat();
|
||||
},
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.refresh_rounded),
|
||||
title: 'Refresh badges and emotes',
|
||||
onTap: () async {
|
||||
await widget.chatStore.getAssets();
|
||||
|
||||
widget.chatStore.updateNotification('Badges and emotes refreshed');
|
||||
},
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: 'Settings',
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Settings(settingsStore: widget.chatStore.settings),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return FrostyBottomSheet(
|
||||
child: MediaQuery.of(context).orientation == Orientation.landscape
|
||||
? SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: ListView(
|
||||
children: children,
|
||||
),
|
||||
)
|
||||
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: children),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,45 +15,59 @@ class ChatModes extends StatelessWidget {
|
|||
children: [
|
||||
Tooltip(
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
message: 'Emote-only mode ${roomState.emoteOnly != '0' ? 'on' : 'off'}',
|
||||
child: Icon(
|
||||
Icons.emoji_emotions_outlined,
|
||||
roomState.emoteOnly != '0' ? Icons.emoji_emotions_rounded : Icons.emoji_emotions_outlined,
|
||||
color: roomState.emoteOnly != '0' ? Colors.yellow : Colors.grey,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
message: roomState.followersOnly == '-1'
|
||||
? 'Followers-only mode off'
|
||||
: roomState.followersOnly == '0'
|
||||
? 'Followers-only mode on'
|
||||
: 'Followers-only mode on (${roomState.followersOnly} ${pluralize('minute', roomState.followersOnly)})',
|
||||
child: Icon(
|
||||
Icons.favorite,
|
||||
roomState.followersOnly != '-1' ? Icons.favorite_rounded : Icons.favorite_outline_rounded,
|
||||
color: roomState.followersOnly != '-1' ? Colors.red : Colors.grey,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
preferBelow: false,
|
||||
message: 'R9K mode ${roomState.r9k != '0' ? 'on' : 'off'}',
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
message: 'R9K (unique-chat) mode ${roomState.r9k != '0' ? 'on' : 'off'}',
|
||||
child: Text(
|
||||
'R9K',
|
||||
style: TextStyle(color: roomState.r9k != '0' ? Colors.purple : Colors.grey),
|
||||
style: TextStyle(
|
||||
color: roomState.r9k != '0' ? Colors.purple : Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
preferBelow: false,
|
||||
message: 'Slow mode ${roomState.slowMode != '0' ? 'on (${roomState.slowMode} ${pluralize('second', roomState.slowMode)})' : 'off'}',
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
message:
|
||||
'Slow mode ${roomState.slowMode != '0' ? 'on (${roomState.slowMode} ${pluralize('second', roomState.slowMode)})' : 'off'}',
|
||||
child: Icon(
|
||||
Icons.history_toggle_off,
|
||||
roomState.slowMode != '0' ? Icons.hourglass_top_rounded : Icons.hourglass_empty_rounded,
|
||||
color: roomState.slowMode != '0' ? Colors.blue : Colors.grey,
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 3),
|
||||
message: 'Subs-only mode ${roomState.subMode != '0' ? 'on' : 'off'}',
|
||||
child: Icon(
|
||||
Icons.attach_money,
|
||||
roomState.subMode != '0' ? Icons.monetization_on_rounded : Icons.monetization_on_outlined,
|
||||
color: roomState.subMode != '0' ? Colors.green : Colors.grey,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:frosty/widgets/section_header.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ChattersList extends StatelessWidget {
|
||||
class ChattersList extends StatefulWidget {
|
||||
final ChatDetailsStore chatDetailsStore;
|
||||
final ChatStore chatStore;
|
||||
final String userLogin;
|
||||
|
|
@ -27,13 +27,18 @@ class ChattersList extends StatelessWidget {
|
|||
required this.userLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ChattersList> createState() => _ChattersListState();
|
||||
}
|
||||
|
||||
class _ChattersListState extends State<ChattersList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const headers = [
|
||||
'Broadcaster',
|
||||
'Staff',
|
||||
'Admins',
|
||||
'Global Moderators',
|
||||
'Global moderators',
|
||||
'Moderators',
|
||||
'VIPs',
|
||||
'Viewers',
|
||||
|
|
@ -41,25 +46,24 @@ class ChattersList extends StatelessWidget {
|
|||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
if (chatDetailsStore.error != null) {
|
||||
if (widget.chatDetailsStore.error != null) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const AlertMessage(
|
||||
message: 'Failed to get chatters',
|
||||
icon: Icons.error,
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
onPressed: chatDetailsStore.updateChatters,
|
||||
child: const Text('Try Again'),
|
||||
onPressed: widget.chatDetailsStore.updateChatters,
|
||||
child: const Text('Try again'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (chatDetailsStore.chatUsers == null) {
|
||||
if (widget.chatDetailsStore.chatUsers == null) {
|
||||
return const LoadingIndicator(subtitle: 'Getting chatters...');
|
||||
}
|
||||
|
||||
|
|
@ -70,21 +74,24 @@ class ChattersList extends StatelessWidget {
|
|||
child: Observer(
|
||||
builder: (context) {
|
||||
return TextField(
|
||||
controller: chatDetailsStore.textController,
|
||||
focusNode: chatDetailsStore.textFieldFocusNode,
|
||||
controller: widget.chatDetailsStore.textController,
|
||||
focusNode: widget.chatDetailsStore.textFieldFocusNode,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
prefixIcon: const Icon(Icons.filter_list),
|
||||
prefixIcon: const Icon(Icons.filter_list_rounded),
|
||||
hintText: 'Filter chatters',
|
||||
suffixIcon: chatDetailsStore.textFieldFocusNode.hasFocus || chatDetailsStore.filterText.isNotEmpty
|
||||
suffixIcon: widget.chatDetailsStore.textFieldFocusNode.hasFocus ||
|
||||
widget.chatDetailsStore.filterText.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: chatDetailsStore.filterText.isEmpty ? 'Cancel' : 'Clear',
|
||||
tooltip: widget.chatDetailsStore.filterText.isEmpty ? 'Cancel' : 'Clear',
|
||||
onPressed: () {
|
||||
if (chatDetailsStore.filterText.isEmpty) chatDetailsStore.textFieldFocusNode.unfocus();
|
||||
chatDetailsStore.textController.clear();
|
||||
if (widget.chatDetailsStore.filterText.isEmpty) {
|
||||
widget.chatDetailsStore.textFieldFocusNode.unfocus();
|
||||
}
|
||||
widget.chatDetailsStore.textController.clear();
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -97,7 +104,7 @@ class ChattersList extends StatelessWidget {
|
|||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
chatDetailsStore.updateChatters();
|
||||
widget.chatDetailsStore.updateChatters();
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
|
|
@ -106,14 +113,14 @@ class ChattersList extends StatelessWidget {
|
|||
builder: (context) {
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: chatDetailsStore.scrollController,
|
||||
controller: widget.chatDetailsStore.scrollController,
|
||||
slivers: [
|
||||
if (chatDetailsStore.filterText.isEmpty)
|
||||
if (widget.chatDetailsStore.filterText.isEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 20.0),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text(
|
||||
'${NumberFormat().format(chatDetailsStore.chatUsers?.chatterCount)} ${chatDetailsStore.chatUsers?.chatterCount == 1 ? 'Chatter' : 'Chatters'}',
|
||||
'${NumberFormat().format(widget.chatDetailsStore.chatUsers?.chatterCount)} ${widget.chatDetailsStore.chatUsers?.chatterCount == 1 ? 'Chatter' : 'Chatters'}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0,
|
||||
|
|
@ -121,28 +128,22 @@ class ChattersList extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (chatDetailsStore.chatUsers?.chatterCount == 0)
|
||||
if (widget.chatDetailsStore.chatUsers?.chatterCount == 0)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: AlertMessage(
|
||||
message: 'No chatters found',
|
||||
icon: Icons.person_off,
|
||||
),
|
||||
child: AlertMessage(message: 'No chatters found'),
|
||||
),
|
||||
)
|
||||
else if (chatDetailsStore.filteredUsers.expand((element) => element).isEmpty)
|
||||
else if (widget.chatDetailsStore.filteredUsers.expand((element) => element).isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: AlertMessage(
|
||||
message: 'No matching chatters',
|
||||
icon: Icons.person_off,
|
||||
),
|
||||
child: AlertMessage(message: 'No matching chatters'),
|
||||
),
|
||||
)
|
||||
else
|
||||
...chatDetailsStore.filteredUsers.expandIndexed(
|
||||
...widget.chatDetailsStore.filteredUsers.expandIndexed(
|
||||
(index, users) => [
|
||||
if (users.isNotEmpty) ...[
|
||||
SliverPadding(
|
||||
|
|
@ -164,16 +165,16 @@ class ChattersList extends StatelessWidget {
|
|||
onLongPress: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
final userInfo = await context
|
||||
.read<TwitchApi>()
|
||||
.getUser(headers: context.read<AuthStore>().headersTwitch, userLogin: users[index]);
|
||||
final userInfo = await context.read<TwitchApi>().getUser(
|
||||
headers: context.read<AuthStore>().headersTwitch,
|
||||
userLogin: users[index]);
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: chatStore,
|
||||
chatStore: widget.chatStore,
|
||||
username: userInfo.login,
|
||||
userId: userInfo.id,
|
||||
displayName: userInfo.displayName,
|
||||
|
|
@ -194,9 +195,11 @@ class ChattersList extends StatelessWidget {
|
|||
Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
child: chatDetailsStore.showJumpButton ? ScrollToTopButton(scrollController: chatDetailsStore.scrollController) : null,
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: widget.chatDetailsStore.showJumpButton
|
||||
? ScrollToTopButton(scrollController: widget.chatDetailsStore.scrollController)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -208,4 +211,10 @@ class ChattersList extends StatelessWidget {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.chatDetailsStore.showJumpButton = false;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_panel.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/recent_emotes_panel.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
|
||||
class EmoteMenu extends StatefulWidget {
|
||||
final ChatStore chatStore;
|
||||
|
||||
const EmoteMenu({
|
||||
Key? key,
|
||||
required this.chatStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<EmoteMenu> createState() => _EmoteMenuState();
|
||||
}
|
||||
|
||||
class _EmoteMenuState extends State<EmoteMenu> {
|
||||
late final PageController _pageContoller = PageController(initialPage: widget.chatStore.assetsStore.emoteMenuIndex);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const sections = [
|
||||
'RECENT',
|
||||
'TWITCH',
|
||||
'7TV',
|
||||
'BTTV',
|
||||
'FFZ',
|
||||
];
|
||||
|
||||
final emoteMenu = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(
|
||||
height: 5.0,
|
||||
thickness: 1.0,
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
...sections.mapIndexed(
|
||||
(index, section) => Padding(
|
||||
padding: const EdgeInsets.only(right: 5.0),
|
||||
child: Observer(
|
||||
builder: (context) => Button(
|
||||
onPressed: () {
|
||||
_pageContoller.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
widget.chatStore.assetsStore.emoteMenuIndex = index;
|
||||
},
|
||||
color: index == widget.chatStore.assetsStore.emoteMenuIndex ? Theme.of(context).colorScheme.secondary : Colors.grey,
|
||||
child: Text(
|
||||
section,
|
||||
style: const TextStyle(
|
||||
letterSpacing: 0.8,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
onPageChanged: (index) => widget.chatStore.assetsStore.emoteMenuIndex = index,
|
||||
controller: _pageContoller,
|
||||
children: [
|
||||
RecentEmotesPanel(
|
||||
chatStore: widget.chatStore,
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
chatStore: widget.chatStore,
|
||||
twitchEmotes: widget.chatStore.assetsStore.userEmoteSectionToEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.sevenTVEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.bttvEmotes,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => EmoteMenuPanel(
|
||||
chatStore: widget.chatStore,
|
||||
emotes: widget.chatStore.assetsStore.ffzEmotes,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
// If pressing the back button on Android while the emote menu is open, close it instead of going back to the streams list.
|
||||
if (widget.chatStore.assetsStore.showEmoteMenu) {
|
||||
widget.chatStore.assetsStore.showEmoteMenu = false;
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
child: emoteMenu,
|
||||
);
|
||||
}
|
||||
|
||||
return emoteMenu;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageContoller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/chat/emote_menu/emote_menu_section.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:frosty/widgets/page_view.dart';
|
||||
|
||||
class EmoteMenuPanel extends StatelessWidget {
|
||||
final ChatStore chatStore;
|
||||
|
|
@ -19,8 +19,12 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (emotes != null) {
|
||||
final globalEmotes =
|
||||
emotes!.where((emote) => emote.type == EmoteType.bttvGlobal || emote.type == EmoteType.ffzGlobal || emote.type == EmoteType.sevenTVGlobal).toList();
|
||||
final globalEmotes = emotes!
|
||||
.where((emote) =>
|
||||
emote.type == EmoteType.bttvGlobal ||
|
||||
emote.type == EmoteType.ffzGlobal ||
|
||||
emote.type == EmoteType.sevenTVGlobal)
|
||||
.toList();
|
||||
|
||||
final channelEmotes = emotes!
|
||||
.where((emote) =>
|
||||
|
|
@ -30,47 +34,24 @@ class EmoteMenuPanel extends StatelessWidget {
|
|||
emote.type == EmoteType.sevenTVChannel)
|
||||
.toList();
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (globalEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Global Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: globalEmotes,
|
||||
),
|
||||
],
|
||||
if (channelEmotes.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader('Channel Emotes'),
|
||||
),
|
||||
return FrostyPageView(
|
||||
headers: ['Global', if (channelEmotes.isNotEmpty) 'Channel'],
|
||||
children: [
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: globalEmotes,
|
||||
),
|
||||
if (channelEmotes.isNotEmpty)
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: channelEmotes,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
for (final entry in twitchEmotes!.entries) ...[
|
||||
SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
entry.key,
|
||||
padding: entry.key == 'Global Emotes' ? const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0) : null,
|
||||
),
|
||||
),
|
||||
EmoteMenuSection(
|
||||
chatStore: chatStore,
|
||||
emotes: entry.value,
|
||||
),
|
||||
]
|
||||
],
|
||||
return FrostyPageView(
|
||||
headers: twitchEmotes!.keys.map((header) => header.split(' ')[0]).toList(),
|
||||
children: twitchEmotes!.entries.map((e) => EmoteMenuSection(chatStore: chatStore, emotes: e.value)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EmoteMenuSection extends StatelessWidget {
|
||||
|
|
@ -18,7 +18,7 @@ class EmoteMenuSection extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverGrid(
|
||||
return GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
|
|
@ -26,26 +26,24 @@ class EmoteMenuSection extends StatelessWidget {
|
|||
? 6
|
||||
: 16,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(emotes[index]),
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: emotes[index].url,
|
||||
height: emotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emotes[index].width?.toDouble(),
|
||||
),
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(emotes[index]),
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Center(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: emotes[index].url,
|
||||
height: emotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emotes[index].width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
childCount: emotes.length,
|
||||
),
|
||||
itemCount: emotes.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RecentEmotesPanel extends StatefulWidget {
|
||||
class RecentEmotesPanel extends StatelessWidget {
|
||||
final ChatStore chatStore;
|
||||
|
||||
const RecentEmotesPanel({
|
||||
|
|
@ -17,68 +14,11 @@ class RecentEmotesPanel extends StatefulWidget {
|
|||
required this.chatStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RecentEmotesPanel> createState() => _RecentEmotesPanelState();
|
||||
}
|
||||
|
||||
class _RecentEmotesPanelState extends State<RecentEmotesPanel> {
|
||||
Future<void> _showClearDialog() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Clear Recent Emotes',
|
||||
content: const Text('Are you sure you want to clear your recent emotes?'),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
setState(widget.chatStore.assetsStore.recentEmotes.clear);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
fill: true,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.red.shade700,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const SectionHeader(
|
||||
'Recent Emotes',
|
||||
padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
height: 25,
|
||||
child: Button(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: widget.chatStore.assetsStore.recentEmotes.isEmpty ? null : _showClearDialog,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
child: const Text(
|
||||
'CLEAR',
|
||||
style: TextStyle(
|
||||
letterSpacing: 0.8,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.chatStore.assetsStore.recentEmotes.isEmpty)
|
||||
if (chatStore.assetsStore.recentEmotes.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: AlertMessage(message: 'No recent emotes'),
|
||||
|
|
@ -94,19 +34,23 @@ class _RecentEmotesPanelState extends State<RecentEmotesPanel> {
|
|||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final emote = widget.chatStore.assetsStore.recentEmotes[index];
|
||||
final validEmotes = [...widget.chatStore.assetsStore.emoteToObject.values, ...widget.chatStore.assetsStore.userEmoteToObject.values];
|
||||
final matchingEmotes = validEmotes.where((existingEmote) => existingEmote.name == emote.name && existingEmote.type == emote.type);
|
||||
final emote = chatStore.assetsStore.recentEmotes[index];
|
||||
final validEmotes = [
|
||||
...chatStore.assetsStore.emoteToObject.values,
|
||||
...chatStore.assetsStore.userEmoteToObject.values
|
||||
];
|
||||
final matchingEmotes = validEmotes
|
||||
.where((existingEmote) => existingEmote.name == emote.name && existingEmote.type == emote.type);
|
||||
|
||||
return InkWell(
|
||||
onTap: matchingEmotes.isNotEmpty ? () => widget.chatStore.addEmote(emote) : null,
|
||||
onTap: matchingEmotes.isNotEmpty ? () => chatStore.addEmote(emote) : null,
|
||||
child: Tooltip(
|
||||
message: emote.name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: matchingEmotes.isNotEmpty ? matchingEmotes.first.url : emote.url,
|
||||
color: matchingEmotes.isNotEmpty ? null : const Color.fromRGBO(255, 255, 255, 0.5),
|
||||
colorBlendMode: matchingEmotes.isNotEmpty ? null : BlendMode.modulate,
|
||||
|
|
@ -118,7 +62,7 @@ class _RecentEmotesPanelState extends State<RecentEmotesPanel> {
|
|||
),
|
||||
);
|
||||
},
|
||||
childCount: widget.chatStore.assetsStore.recentEmotes.length,
|
||||
childCount: chatStore.assetsStore.recentEmotes.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -63,16 +63,14 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
@readonly
|
||||
var _userToBTTVBadges = <String, Badge>{};
|
||||
|
||||
/// The current index of the emote menu stack.
|
||||
@observable
|
||||
var emoteMenuIndex = 0;
|
||||
|
||||
/// Whether or not the emote menu is visible.
|
||||
@observable
|
||||
var showEmoteMenu = false;
|
||||
|
||||
bool isBTTV(Emote emote) {
|
||||
return emote.type == EmoteType.bttvChannel || emote.type == EmoteType.bttvGlobal || emote.type == EmoteType.bttvShared;
|
||||
return emote.type == EmoteType.bttvChannel ||
|
||||
emote.type == EmoteType.bttvGlobal ||
|
||||
emote.type == EmoteType.bttvShared;
|
||||
}
|
||||
|
||||
bool isFFZ(Emote emote) {
|
||||
|
|
@ -97,8 +95,12 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
// Retrieve the instance that will allow us to retrieve local search history.
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
_recentEmotes =
|
||||
prefs.getStringList('recent_emotes')?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson))).toList().asObservable() ?? ObservableList<Emote>();
|
||||
_recentEmotes = prefs
|
||||
.getStringList('recent_emotes')
|
||||
?.map((emoteJson) => Emote.fromJson(jsonDecode(emoteJson)))
|
||||
.toList()
|
||||
.asObservable() ??
|
||||
ObservableList<Emote>();
|
||||
|
||||
_disposeReaction = autorun((_) {
|
||||
if (_recentEmotes.length > 48) _recentEmotes.removeLast();
|
||||
|
|
@ -150,7 +152,9 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
ffzRoomInfo = ffzRoom.item1;
|
||||
return ffzRoom.item2;
|
||||
}).catchError(onError),
|
||||
]).then((assets) => assets.expand((list) => list)).then((emotes) => _emoteToObject = {for (final emote in emotes) emote.name: emote});
|
||||
])
|
||||
.then((assets) => assets.expand((list) => list))
|
||||
.then((emotes) => _emoteToObject = {for (final emote in emotes) emote.name: emote});
|
||||
|
||||
@action
|
||||
Future<void> badgesFuture({
|
||||
|
|
@ -161,10 +165,10 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
Future.wait([
|
||||
// Get global badges first, then channel badges to avoid badge conflicts.
|
||||
// We want the channel badges to override the global badges.
|
||||
twitchApi
|
||||
.getBadgesGlobal()
|
||||
twitchApi.getBadgesGlobal().then((badges) => twitchBadgesToObject.addAll(badges)).then((_) => twitchApi
|
||||
.getBadgesChannel(id: channelId)
|
||||
.then((badges) => twitchBadgesToObject.addAll(badges))
|
||||
.then((_) => twitchApi.getBadgesChannel(id: channelId).then((badges) => twitchBadgesToObject.addAll(badges)).catchError(onError)),
|
||||
.catchError(onError)),
|
||||
ffzApi.getBadges().then((badges) => _userToFFZBadges = badges).catchError(onError),
|
||||
sevenTVApi.getBadges().then((badges) => _userTo7TVBadges = badges).catchError(onError),
|
||||
bttvApi.getBadges().then((badges) => _userToBTTVBadges = badges).catchError(onError),
|
||||
|
|
@ -176,7 +180,8 @@ abstract class ChatAssetsStoreBase with Store {
|
|||
required Map<String, String> headers,
|
||||
required Function onError,
|
||||
}) async {
|
||||
final userEmotes = await Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError)));
|
||||
final userEmotes = await Future.wait(
|
||||
emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers).catchError(onError)));
|
||||
|
||||
for (final emoteSet in userEmotes) {
|
||||
if (emoteSet.isNotEmpty) {
|
||||
|
|
|
|||
|
|
@ -159,22 +159,6 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$emoteMenuIndexAtom =
|
||||
Atom(name: 'ChatAssetsStoreBase.emoteMenuIndex', context: context);
|
||||
|
||||
@override
|
||||
int get emoteMenuIndex {
|
||||
_$emoteMenuIndexAtom.reportRead();
|
||||
return super.emoteMenuIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteMenuIndex(int value) {
|
||||
_$emoteMenuIndexAtom.reportWrite(value, super.emoteMenuIndex, () {
|
||||
super.emoteMenuIndex = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showEmoteMenuAtom =
|
||||
Atom(name: 'ChatAssetsStoreBase.showEmoteMenu', context: context);
|
||||
|
||||
|
|
@ -266,7 +250,6 @@ mixin _$ChatAssetsStore on ChatAssetsStoreBase, Store {
|
|||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
emoteMenuIndex: ${emoteMenuIndex},
|
||||
showEmoteMenu: ${showEmoteMenu},
|
||||
bttvEmotes: ${bttvEmotes},
|
||||
ffzEmotes: ${ffzEmotes},
|
||||
|
|
|
|||
|
|
@ -86,6 +86,21 @@ abstract class ChatStoreBase with Store {
|
|||
/// Timer used for dismissing the notification.
|
||||
Timer? _notificationTimer;
|
||||
|
||||
/// The current timer for the sleep timer if active.
|
||||
Timer? sleepTimer;
|
||||
|
||||
/// The amount of hours the sleep timer is set to.
|
||||
@observable
|
||||
var sleepHours = 0;
|
||||
|
||||
/// The amount of minutes the sleep timer is set to.
|
||||
@observable
|
||||
var sleepMinutes = 0;
|
||||
|
||||
/// The time remaining for the sleep timer.
|
||||
@observable
|
||||
var timeRemaining = const Duration();
|
||||
|
||||
/// A notification message to display above the chat.
|
||||
@readonly
|
||||
String? _notification;
|
||||
|
|
@ -158,7 +173,8 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
if (settings.chatDelay > 0) {
|
||||
_messageBuffer.add(IRCMessage.createNotice(
|
||||
message: 'Waiting ${settings.chatDelay.toInt()} ${settings.chatDelay == 1.0 ? 'second' : 'seconds'} due to message delay setting...'));
|
||||
message:
|
||||
'Waiting ${settings.chatDelay.toInt()} ${settings.chatDelay == 1.0 ? 'second' : 'seconds'} due to message delay setting...'));
|
||||
}
|
||||
|
||||
connectToChat();
|
||||
|
|
@ -189,8 +205,8 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
// Add a listener to the textfield that will show/hide the autocomplete bar if focused.
|
||||
// Will also rebuild the autocomplete bar when typing, refreshing the results as the user types.
|
||||
textController
|
||||
.addListener(() => _showEmoteAutocomplete = !_showMentionAutocomplete && textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty);
|
||||
textController.addListener(() => _showEmoteAutocomplete =
|
||||
!_showMentionAutocomplete && textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty);
|
||||
|
||||
textController.addListener(() {
|
||||
_showSendButton = textController.text.isNotEmpty;
|
||||
|
|
@ -214,7 +230,9 @@ abstract class ChatStoreBase with Store {
|
|||
// Filter messages from any blocked users if not a moderator or not the channel owner.
|
||||
if (!_userState.mod &&
|
||||
channelName != auth.user.details?.login &&
|
||||
auth.user.blockedUsers.where((blockedUser) => blockedUser.userLogin == parsedIRCMessage.user).isNotEmpty) continue;
|
||||
auth.user.blockedUsers.where((blockedUser) => blockedUser.userLogin == parsedIRCMessage.user).isNotEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (parsedIRCMessage.command) {
|
||||
case Command.privateMessage:
|
||||
|
|
@ -280,7 +298,9 @@ abstract class ChatStoreBase with Store {
|
|||
_channel?.sink.add('PONG :tmi.twitch.tv');
|
||||
return;
|
||||
} else if (message.contains('Welcome, GLHF!')) {
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: "Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!"));
|
||||
_messageBuffer.add(IRCMessage.createNotice(
|
||||
message:
|
||||
"Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!"));
|
||||
|
||||
getAssets();
|
||||
|
||||
|
|
@ -334,7 +354,8 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
if (_backoffTime > 0) {
|
||||
// Add notice that chat was disconnected and then wait the backoff time before reconnecting.
|
||||
final notice = 'Disconnected from chat, waiting $_backoffTime ${_backoffTime == 1 ? 'second' : 'seconds'} before reconnecting...';
|
||||
final notice =
|
||||
'Disconnected from chat, waiting $_backoffTime ${_backoffTime == 1 ? 'second' : 'seconds'} before reconnecting...';
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: notice));
|
||||
}
|
||||
|
||||
|
|
@ -451,10 +472,52 @@ abstract class ChatStoreBase with Store {
|
|||
_notificationTimer = Timer(const Duration(seconds: 2), () => _notification = null);
|
||||
}
|
||||
|
||||
/// Updates the sleep timer with [sleepHours] and [sleepMinutes].
|
||||
/// Calls [onTimerFinished] when the sleep timer completes.
|
||||
@action
|
||||
void updateSleepTimer({required void Function() onTimerFinished}) {
|
||||
// If hours and minutes are 0, do nothing.
|
||||
if (sleepHours == 0 && sleepMinutes == 0) return;
|
||||
|
||||
// If there is an ongoing timer, cancel it since it'll be replaced.
|
||||
if (sleepTimer != null) cancelSleepTimer();
|
||||
|
||||
// Update the new time remaining
|
||||
timeRemaining = Duration(hours: sleepHours, minutes: sleepMinutes);
|
||||
|
||||
// Reset the hours and minutes in the dropdown buttons.
|
||||
sleepHours = 0;
|
||||
sleepMinutes = 0;
|
||||
|
||||
// Set a periodic timer that will update the time remaining every second.
|
||||
sleepTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(timer) {
|
||||
// If the timer is up, cancel the timer and exit the app.
|
||||
if (timeRemaining.inSeconds == 0) {
|
||||
timer.cancel();
|
||||
onTimerFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrement the time remaining.
|
||||
timeRemaining = Duration(seconds: timeRemaining.inSeconds - 1);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Cancels the sleep timer and resets the time remaining.
|
||||
@action
|
||||
void cancelSleepTimer() {
|
||||
sleepTimer?.cancel();
|
||||
timeRemaining = const Duration();
|
||||
}
|
||||
|
||||
/// Closes and disposes all the channels and controllers used by the store.
|
||||
void dispose() {
|
||||
_messageBufferTimer.cancel();
|
||||
_notificationTimer?.cancel();
|
||||
sleepTimer?.cancel();
|
||||
|
||||
_channel?.sink.close(1001);
|
||||
_channel = null;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,54 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
name: 'ChatStoreBase.renderMessages'))
|
||||
.value;
|
||||
|
||||
late final _$sleepHoursAtom =
|
||||
Atom(name: 'ChatStoreBase.sleepHours', context: context);
|
||||
|
||||
@override
|
||||
int get sleepHours {
|
||||
_$sleepHoursAtom.reportRead();
|
||||
return super.sleepHours;
|
||||
}
|
||||
|
||||
@override
|
||||
set sleepHours(int value) {
|
||||
_$sleepHoursAtom.reportWrite(value, super.sleepHours, () {
|
||||
super.sleepHours = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$sleepMinutesAtom =
|
||||
Atom(name: 'ChatStoreBase.sleepMinutes', context: context);
|
||||
|
||||
@override
|
||||
int get sleepMinutes {
|
||||
_$sleepMinutesAtom.reportRead();
|
||||
return super.sleepMinutes;
|
||||
}
|
||||
|
||||
@override
|
||||
set sleepMinutes(int value) {
|
||||
_$sleepMinutesAtom.reportWrite(value, super.sleepMinutes, () {
|
||||
super.sleepMinutes = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$timeRemainingAtom =
|
||||
Atom(name: 'ChatStoreBase.timeRemaining', context: context);
|
||||
|
||||
@override
|
||||
Duration get timeRemaining {
|
||||
_$timeRemainingAtom.reportRead();
|
||||
return super.timeRemaining;
|
||||
}
|
||||
|
||||
@override
|
||||
set timeRemaining(Duration value) {
|
||||
_$timeRemainingAtom.reportWrite(value, super.timeRemaining, () {
|
||||
super.timeRemaining = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_notificationAtom =
|
||||
Atom(name: 'ChatStoreBase._notification', context: context);
|
||||
|
||||
|
|
@ -249,9 +297,34 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSleepTimer({required void Function() onTimerFinished}) {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
name: 'ChatStoreBase.updateSleepTimer');
|
||||
try {
|
||||
return super.updateSleepTimer(onTimerFinished: onTimerFinished);
|
||||
} finally {
|
||||
_$ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void cancelSleepTimer() {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
name: 'ChatStoreBase.cancelSleepTimer');
|
||||
try {
|
||||
return super.cancelSleepTimer();
|
||||
} finally {
|
||||
_$ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
sleepHours: ${sleepHours},
|
||||
sleepMinutes: ${sleepMinutes},
|
||||
timeRemaining: ${timeRemaining},
|
||||
expandChat: ${expandChat},
|
||||
renderMessages: ${renderMessages}
|
||||
''';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_details.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
|
||||
class ChatBottomBar extends StatelessWidget {
|
||||
final ChatStore chatStore;
|
||||
|
|
@ -13,14 +13,17 @@ class ChatBottomBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final emoteMenuButton = IconButton(
|
||||
color: chatStore.assetsStore.showEmoteMenu ? Theme.of(context).colorScheme.secondary : null,
|
||||
tooltip: 'Emote menu',
|
||||
icon: const Icon(Icons.emoji_emotions_outlined),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu = !chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
final emoteMenuButton = Tooltip(
|
||||
message: 'Emote menu',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
color: chatStore.assetsStore.showEmoteMenu ? Theme.of(context).colorScheme.secondary : null,
|
||||
icon: Icon(chatStore.assetsStore.showEmoteMenu ? Icons.emoji_emotions_rounded : Icons.emoji_emotions_outlined),
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
chatStore.assetsStore.showEmoteMenu = !chatStore.assetsStore.showEmoteMenu;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return Observer(
|
||||
|
|
@ -30,10 +33,14 @@ class ChatBottomBar extends StatelessWidget {
|
|||
...chatStore.assetsStore.bttvEmotes,
|
||||
...chatStore.assetsStore.ffzEmotes,
|
||||
...chatStore.assetsStore.sevenTVEmotes
|
||||
].where((emote) => emote.name.toLowerCase().contains(chatStore.textController.text.split(' ').last.toLowerCase())).toList();
|
||||
]
|
||||
.where((emote) =>
|
||||
emote.name.toLowerCase().contains(chatStore.textController.text.split(' ').last.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
final matchingChatters = chatStore.chatDetailsStore.allChatters
|
||||
.where((chatter) => chatter.contains(chatStore.textController.text.split(' ').last.replaceFirst('@', '').toLowerCase()))
|
||||
.where((chatter) =>
|
||||
chatter.contains(chatStore.textController.text.split(' ').last.replaceFirst('@', '').toLowerCase()))
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
|
|
@ -57,9 +64,9 @@ class ChatBottomBar extends StatelessWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: matchingEmotes[index].url,
|
||||
fadeInDuration: const Duration(),
|
||||
useFade: false,
|
||||
height: matchingEmotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: matchingEmotes[index].width?.toDouble(),
|
||||
),
|
||||
|
|
@ -70,7 +77,9 @@ class ChatBottomBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
],
|
||||
if (chatStore.settings.autocomplete && chatStore.showMentionAutocomplete && matchingChatters.isNotEmpty) ...[
|
||||
if (chatStore.settings.autocomplete &&
|
||||
chatStore.showMentionAutocomplete &&
|
||||
matchingChatters.isNotEmpty) ...[
|
||||
const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
|
|
@ -89,7 +98,8 @@ class ChatBottomBar extends StatelessWidget {
|
|||
..add('@${matchingChatters[index]} ');
|
||||
|
||||
chatStore.textController.text = split.join(' ');
|
||||
chatStore.textController.selection = TextSelection.fromPosition(TextPosition(offset: chatStore.textController.text.length));
|
||||
chatStore.textController.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: chatStore.textController.text.length));
|
||||
},
|
||||
child: Text(matchingChatters[index]),
|
||||
),
|
||||
|
|
@ -105,12 +115,12 @@ class ChatBottomBar extends StatelessWidget {
|
|||
chatStore.settings.showVideo &&
|
||||
MediaQuery.of(context).orientation == Orientation.landscape)
|
||||
IconButton(
|
||||
tooltip: 'Send a message',
|
||||
tooltip: 'Enter a message',
|
||||
onPressed: () {
|
||||
chatStore.expandChat = true;
|
||||
chatStore.textFieldFocusNode.requestFocus();
|
||||
},
|
||||
icon: const Icon(Icons.chat),
|
||||
icon: const Icon(Icons.edit),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
|
|
@ -121,7 +131,7 @@ class ChatBottomBar extends StatelessWidget {
|
|||
maxLines: 5,
|
||||
enabled: chatStore.auth.isLoggedIn ? true : false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.fromLTRB(15.0, 10.0, 0.0, 10.0),
|
||||
contentPadding: const EdgeInsets.only(left: 15.0),
|
||||
prefixIcon: chatStore.settings.emoteMenuButtonOnLeft ? emoteMenuButton : null,
|
||||
suffixIcon: chatStore.settings.emoteMenuButtonOnLeft ? null : emoteMenuButton,
|
||||
hintMaxLines: 1,
|
||||
|
|
@ -134,15 +144,18 @@ class ChatBottomBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (chatStore.showSendButton &&
|
||||
(chatStore.settings.chatWidth >= 0.3 || chatStore.expandChat || MediaQuery.of(context).orientation == Orientation.portrait))
|
||||
(chatStore.settings.chatWidth >= 0.3 ||
|
||||
chatStore.expandChat ||
|
||||
MediaQuery.of(context).orientation == Orientation.portrait))
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send),
|
||||
onPressed: chatStore.auth.isLoggedIn ? () => chatStore.sendMessage(chatStore.textController.text) : null,
|
||||
icon: const Icon(Icons.send_rounded),
|
||||
onPressed:
|
||||
chatStore.auth.isLoggedIn ? () => chatStore.sendMessage(chatStore.textController.text) : null,
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more),
|
||||
icon: Icon(Icons.adaptive.more_rounded),
|
||||
tooltip: 'More',
|
||||
onPressed: () => showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
|
|
|
|||
|
|
@ -20,15 +20,13 @@ class ChatMessage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void onLongPressName() {
|
||||
void onTapName() {
|
||||
// Ignore if the message is a recent message in the modal bottom sheet.
|
||||
if (isModal) return;
|
||||
|
||||
// Ignore if long-pressing own username.
|
||||
if (ircMessage.user == null || ircMessage.user == chatStore.auth.user.details?.login) return;
|
||||
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
|
|
@ -62,12 +60,14 @@ class ChatMessage extends StatelessWidget {
|
|||
case Command.userState:
|
||||
// If user is being mentioned in the message, highlight it red.
|
||||
if (ircMessage.mention == true) color = Colors.red.withOpacity(0.2);
|
||||
if (chatStore.settings.highlightFirstTimeChatter && ircMessage.tags['first-msg'] == '1') color = Colors.green.withOpacity(0.2);
|
||||
if (chatStore.settings.highlightFirstTimeChatter && ircMessage.tags['first-msg'] == '1') {
|
||||
color = Colors.green.withOpacity(0.2);
|
||||
}
|
||||
|
||||
final messageSpan = Text.rich(
|
||||
TextSpan(
|
||||
children: ircMessage.generateSpan(
|
||||
onLongPressName: onLongPressName,
|
||||
onTapName: onTapName,
|
||||
style: defaultTextStyle,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
emoteScale: chatStore.settings.emoteScale,
|
||||
|
|
@ -84,14 +84,15 @@ class ChatMessage extends StatelessWidget {
|
|||
final replyUser = ircMessage.tags['reply-parent-display-name'];
|
||||
final replyBody = ircMessage.tags['reply-parent-msg-body'];
|
||||
|
||||
if ((replyUser != null && replyBody != null) || (chatStore.settings.highlightFirstTimeChatter && ircMessage.tags['first-msg'] == '1')) {
|
||||
if ((replyUser != null && replyBody != null) ||
|
||||
(chatStore.settings.highlightFirstTimeChatter && ircMessage.tags['first-msg'] == '1')) {
|
||||
renderMessage = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
replyUser != null && replyBody != null ? Icons.reply : Icons.new_releases_outlined,
|
||||
replyUser != null && replyBody != null ? Icons.reply_rounded : Icons.new_releases_outlined,
|
||||
size: defaultBadgeSize * chatStore.settings.badgeScale,
|
||||
color: defaultTextStyle.color?.withOpacity(0.5),
|
||||
textDirection: TextDirection.rtl,
|
||||
|
|
@ -102,6 +103,8 @@ class ChatMessage extends StatelessWidget {
|
|||
? Tooltip(
|
||||
message: 'Replying to @$replyUser: $replyBody',
|
||||
preferBelow: false,
|
||||
triggerMode: TooltipTriggerMode.tap,
|
||||
showDuration: const Duration(seconds: 5),
|
||||
child: Text(
|
||||
'Replying to @$replyUser: $replyBody',
|
||||
maxLines: 1,
|
||||
|
|
@ -160,7 +163,7 @@ class ChatMessage extends StatelessWidget {
|
|||
Text.rich(
|
||||
TextSpan(
|
||||
children: ircMessage.generateSpan(
|
||||
onLongPressName: onLongPressName,
|
||||
onTapName: onTapName,
|
||||
style: defaultTextStyle,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
emoteScale: chatStore.settings.emoteScale,
|
||||
|
|
@ -214,7 +217,7 @@ class ChatMessage extends StatelessWidget {
|
|||
Text.rich(
|
||||
TextSpan(
|
||||
children: ircMessage.generateSpan(
|
||||
onLongPressName: onLongPressName,
|
||||
onTapName: onTapName,
|
||||
style: defaultTextStyle,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
emoteScale: chatStore.settings.emoteScale,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/bottom_sheet.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/modal.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
|
|
@ -32,9 +31,10 @@ class ChatUserModal extends StatefulWidget {
|
|||
class _ChatUserModalState extends State<ChatUserModal> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final name = regexEnglish.hasMatch(widget.displayName) ? widget.displayName : '${widget.displayName} (${widget.username})';
|
||||
final name =
|
||||
regexEnglish.hasMatch(widget.displayName) ? widget.displayName : '${widget.displayName} (${widget.username})';
|
||||
|
||||
return FrostyModal(
|
||||
return FrostyBottomSheet(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
child: Column(
|
||||
|
|
@ -49,7 +49,6 @@ class _ChatUserModalState extends State<ChatUserModal> {
|
|||
children: [
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
preferBelow: false,
|
||||
message: name,
|
||||
child: Text(
|
||||
name,
|
||||
|
|
@ -71,29 +70,26 @@ class _ChatUserModalState extends State<ChatUserModal> {
|
|||
child: const Text('Reply'),
|
||||
)
|
||||
: null,
|
||||
onLongPress: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => BlockReportModal(
|
||||
authStore: widget.chatStore.auth,
|
||||
name: name,
|
||||
userLogin: widget.username,
|
||||
userId: widget.userId,
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => BlockReportModal(
|
||||
authStore: widget.chatStore.auth,
|
||||
name: name,
|
||||
userLogin: widget.username,
|
||||
userId: widget.userId,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SectionHeader(
|
||||
'Recent Messages',
|
||||
'Recent messages',
|
||||
padding: EdgeInsets.all(10.0),
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
final userMessages = widget.chatStore.messages.reversed.where((message) => message.user == widget.username).toList();
|
||||
final userMessages =
|
||||
widget.chatStore.messages.reversed.where((message) => message.user == widget.username).toList();
|
||||
|
||||
if (userMessages.isEmpty) {
|
||||
return const AlertMessage(message: 'No recent messages');
|
||||
|
|
|
|||
107
lib/screens/channel/video/video_bar.dart
Normal file
107
lib/screens/channel/video/video_bar.dart
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/screens/home/top/categories/category_streams.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
|
||||
class VideoBar extends StatelessWidget {
|
||||
final StreamTwitch streamInfo;
|
||||
final bool showCategory;
|
||||
final bool tappableCategory;
|
||||
final Color? titleTextColor;
|
||||
final Color? subtitleTextColor;
|
||||
final FontWeight? subtitleTextWeight;
|
||||
|
||||
const VideoBar({
|
||||
Key? key,
|
||||
required this.streamInfo,
|
||||
this.showCategory = true,
|
||||
this.tappableCategory = true,
|
||||
this.titleTextColor,
|
||||
this.subtitleTextColor,
|
||||
this.subtitleTextWeight,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final streamTitle = streamInfo.title.trim();
|
||||
final category = streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'No Category';
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName)
|
||||
? streamInfo.userName
|
||||
: '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ProfilePicture(userLogin: streamInfo.userLogin),
|
||||
const SizedBox(width: 10.0),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: 'Streamer: $streamerName',
|
||||
showDuration: const Duration(seconds: 3),
|
||||
child: Text(
|
||||
streamerName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: titleTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Tooltip(
|
||||
message: 'Title: ${streamInfo.title.trim()}',
|
||||
showDuration: const Duration(seconds: 5),
|
||||
child: Text(
|
||||
streamTitle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: subtitleTextColor ?? Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||
fontWeight: subtitleTextWeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showCategory) ...[
|
||||
const SizedBox(height: 5.0),
|
||||
InkWell(
|
||||
onTap: tappableCategory && streamInfo.gameName.isNotEmpty
|
||||
? () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CategoryStreams(
|
||||
categoryName: streamInfo.gameName,
|
||||
categoryId: streamInfo.gameId,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Tooltip(
|
||||
message: 'Category: ${streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'None'}',
|
||||
showDuration: const Duration(seconds: 3),
|
||||
child: Text(
|
||||
category,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: subtitleTextColor ?? Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||
fontWeight: subtitleTextWeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,96 +3,25 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/screens/channel/chat/details/chat_users_list.dart';
|
||||
import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/channel/video/video_bar.dart';
|
||||
import 'package:frosty/screens/channel/video/video_store.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:frosty/widgets/bottom_sheet.dart';
|
||||
import 'package:frosty/widgets/uptime.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Creates a widget containing controls which enable interactions with an underlying [Video] widget.
|
||||
class VideoOverlay extends StatelessWidget {
|
||||
final VideoStore videoStore;
|
||||
final void Function() onSettingsPressed;
|
||||
final ChatStore chatStore;
|
||||
|
||||
const VideoOverlay({
|
||||
Key? key,
|
||||
required this.videoStore,
|
||||
required this.onSettingsPressed,
|
||||
required this.chatStore,
|
||||
}) : super(key: key);
|
||||
|
||||
Future<void> _showSleepTimerDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Sleep Timer',
|
||||
content: Observer(
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.timer),
|
||||
Text(' ${videoStore.timeRemaining.toString().split('.')[0]}'),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'Cancel sleep timer',
|
||||
onPressed: videoStore.sleepTimer != null && videoStore.sleepTimer!.isActive ? videoStore.cancelSleepTimer : null,
|
||||
icon: const Icon(Icons.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
DropdownButton(
|
||||
value: videoStore.sleepHours,
|
||||
items: List.generate(24, (index) => index).map((e) => DropdownMenuItem(value: e, child: Text(e.toString()))).toList(),
|
||||
onChanged: (int? hours) => videoStore.sleepHours = hours!,
|
||||
menuMaxHeight: 200,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
const Text('Hours'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
DropdownButton(
|
||||
value: videoStore.sleepMinutes,
|
||||
items: List.generate(60, (index) => index).map((e) => DropdownMenuItem(value: e, child: Text(e.toString()))).toList(),
|
||||
onChanged: (int? minutes) => videoStore.sleepMinutes = minutes!,
|
||||
menuMaxHeight: 200,
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
const Text('Minutes'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Observer(
|
||||
builder: (context) => Button(
|
||||
onPressed: videoStore.sleepHours == 0 && videoStore.sleepMinutes == 0
|
||||
? null
|
||||
: () => videoStore.updateSleepTimer(
|
||||
onTimerFinished: () => navigatorKey.currentState?.popUntil((route) => route.isFirst),
|
||||
),
|
||||
child: const Text('Set Timer'),
|
||||
),
|
||||
),
|
||||
Button(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
fill: true,
|
||||
color: Colors.red.shade700,
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
|
|
@ -100,88 +29,77 @@ class VideoOverlay extends StatelessWidget {
|
|||
final backButton = IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back,
|
||||
Icons.adaptive.arrow_back_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
);
|
||||
|
||||
final settingsButton = IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: const Icon(
|
||||
Icons.settings,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: onSettingsPressed,
|
||||
);
|
||||
|
||||
final chatOverlayButton = Observer(
|
||||
builder: (_) => IconButton(
|
||||
tooltip: videoStore.settingsStore.fullScreenChatOverlay ? 'Hide chat overlay' : 'Show chat overlay',
|
||||
onPressed: () => videoStore.settingsStore.fullScreenChatOverlay = !videoStore.settingsStore.fullScreenChatOverlay,
|
||||
icon: videoStore.settingsStore.fullScreenChatOverlay ? const Icon(Icons.chat_bubble_outline) : const Icon(Icons.chat_bubble),
|
||||
onPressed: () =>
|
||||
videoStore.settingsStore.fullScreenChatOverlay = !videoStore.settingsStore.fullScreenChatOverlay,
|
||||
icon: videoStore.settingsStore.fullScreenChatOverlay
|
||||
? const Icon(Icons.chat_rounded)
|
||||
: const Icon(Icons.chat_outlined),
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
|
||||
final refreshButton = IconButton(
|
||||
tooltip: 'Refresh',
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.white,
|
||||
final refreshButton = Tooltip(
|
||||
message: 'Refresh',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.refresh_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.handleRefresh,
|
||||
),
|
||||
onPressed: videoStore.handleRefresh,
|
||||
);
|
||||
|
||||
final fullScreenButton = IconButton(
|
||||
tooltip: videoStore.settingsStore.fullScreen ? 'Exit fullscreen mode' : 'Enter fullscreen mode',
|
||||
icon: videoStore.settingsStore.fullScreen
|
||||
? const Icon(
|
||||
Icons.fullscreen_exit,
|
||||
color: Colors.white,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.fullscreen,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => videoStore.settingsStore.fullScreen = !videoStore.settingsStore.fullScreen,
|
||||
final fullScreenButton = Tooltip(
|
||||
message: videoStore.settingsStore.fullScreen ? 'Exit fullscreen mode' : 'Enter fullscreen mode',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
videoStore.settingsStore.fullScreen ? Icons.fullscreen_exit_rounded : Icons.fullscreen_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => videoStore.settingsStore.fullScreen = !videoStore.settingsStore.fullScreen,
|
||||
),
|
||||
);
|
||||
|
||||
final sleepTimerButton = IconButton(
|
||||
tooltip: 'Sleep timer',
|
||||
icon: const Icon(
|
||||
Icons.timer,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => _showSleepTimerDialog(context),
|
||||
);
|
||||
|
||||
final rotateButton = IconButton(
|
||||
tooltip: orientation == Orientation.portrait ? 'Enter landscape mode' : 'Exit landscape mode',
|
||||
icon: const Icon(
|
||||
Icons.screen_rotation,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
if (orientation == Orientation.portrait) {
|
||||
if (Platform.isIOS) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
SystemChrome.setPreferredOrientations([]);
|
||||
final rotateButton = Tooltip(
|
||||
message: orientation == Orientation.portrait ? 'Enter landscape mode' : 'Exit landscape mode',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.screen_rotation_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
if (orientation == Orientation.portrait) {
|
||||
if (Platform.isIOS) {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeRight,
|
||||
]);
|
||||
SystemChrome.setPreferredOrientations([]);
|
||||
} else {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
SystemChrome.setPreferredOrientations([]);
|
||||
}
|
||||
} else {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
SystemChrome.setPreferredOrientations([]);
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final streamInfo = videoStore.streamInfo;
|
||||
|
|
@ -194,7 +112,6 @@ class VideoOverlay extends StatelessWidget {
|
|||
backButton,
|
||||
const Spacer(),
|
||||
if (videoStore.settingsStore.fullScreen && orientation == Orientation.landscape) chatOverlayButton,
|
||||
settingsButton,
|
||||
],
|
||||
),
|
||||
Align(
|
||||
|
|
@ -212,11 +129,6 @@ class VideoOverlay extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final streamTitle = videoStore.streamInfo!.title.trim();
|
||||
final category = videoStore.streamInfo!.gameName.isNotEmpty ? videoStore.streamInfo!.gameName : 'No Category';
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName) ? streamInfo.userName : '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Stack(
|
||||
|
|
@ -225,72 +137,30 @@ class VideoOverlay extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
backButton,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ProfilePicture(
|
||||
userLogin: streamInfo.userLogin,
|
||||
radius: 10,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
message: streamerName,
|
||||
child: Text(
|
||||
streamerName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Tooltip(
|
||||
message: streamTitle,
|
||||
child: Text(
|
||||
streamTitle,
|
||||
maxLines: orientation == Orientation.portrait ? 1 : 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (orientation == Orientation.landscape)
|
||||
Expanded(
|
||||
child: VideoBar(
|
||||
streamInfo: streamInfo,
|
||||
titleTextColor: Colors.white,
|
||||
subtitleTextColor: Colors.white,
|
||||
subtitleTextWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (videoStore.settingsStore.fullScreen && orientation == Orientation.landscape) chatOverlayButton,
|
||||
sleepTimerButton,
|
||||
settingsButton,
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: IconButton(
|
||||
tooltip: videoStore.paused ? 'Play' : 'Pause',
|
||||
iconSize: 50.0,
|
||||
icon: videoStore.paused
|
||||
? const Icon(
|
||||
Icons.play_arrow,
|
||||
color: Colors.white,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.pause,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.handlePausePlay,
|
||||
child: Tooltip(
|
||||
message: videoStore.paused ? 'Play' : 'Pause',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
iconSize: 50.0,
|
||||
icon: Icon(
|
||||
videoStore.paused ? Icons.play_arrow_rounded : Icons.pause_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.handlePausePlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
|
|
@ -300,62 +170,62 @@ class VideoOverlay extends StatelessWidget {
|
|||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0, left: 8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
padding: const EdgeInsets.only(left: 10.0, bottom: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.games,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
message: category,
|
||||
preferBelow: false,
|
||||
child: Text(
|
||||
category,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
Tooltip(
|
||||
message: 'Stream uptime',
|
||||
preferBelow: false,
|
||||
child: Row(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(2.0),
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
color: Colors.red,
|
||||
size: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3.0),
|
||||
Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Tooltip(
|
||||
message: 'Viewer count',
|
||||
preferBelow: false,
|
||||
child: GestureDetector(
|
||||
onTap: () => showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => FrostyBottomSheet(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: ChattersList(
|
||||
chatDetailsStore: chatStore.chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: streamInfo.userLogin,
|
||||
),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.circle,
|
||||
color: Colors.red,
|
||||
Icons.visibility,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.people,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
|
|
@ -367,19 +237,22 @@ class VideoOverlay extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Picture-in-picture',
|
||||
icon: const Icon(
|
||||
Icons.picture_in_picture_alt_rounded,
|
||||
color: Colors.white,
|
||||
Tooltip(
|
||||
message: 'Enter picture-in-picture',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.picture_in_picture_alt_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.requestPictureInPicture,
|
||||
),
|
||||
onPressed: videoStore.requestPictureInPicture,
|
||||
),
|
||||
refreshButton,
|
||||
if (!videoStore.isIPad) rotateButton,
|
||||
|
|
|
|||
|
|
@ -32,9 +32,6 @@ abstract class VideoStoreBase with Store {
|
|||
/// The webview controller used for injecting JavaScript to control the webview and video player.
|
||||
WebViewController? controller;
|
||||
|
||||
/// The current timer for the sleep timer if active.
|
||||
Timer? sleepTimer;
|
||||
|
||||
/// The timer that handles hiding the overlay automatically
|
||||
late Timer _overlayTimer;
|
||||
|
||||
|
|
@ -69,18 +66,6 @@ abstract class VideoStoreBase with Store {
|
|||
return NavigationDecision.prevent;
|
||||
}
|
||||
|
||||
/// The amount of hours the sleep timer is set to.
|
||||
@observable
|
||||
var sleepHours = 0;
|
||||
|
||||
/// The amount of minutes the sleep timer is set to.
|
||||
@observable
|
||||
var sleepMinutes = 0;
|
||||
|
||||
/// The time remaining for the sleep timer.
|
||||
@observable
|
||||
var timeRemaining = const Duration();
|
||||
|
||||
/// If the video is currently paused.
|
||||
///
|
||||
/// Does not pause or play the video, only used for rendering state of the overlay.
|
||||
|
|
@ -135,8 +120,10 @@ abstract class VideoStoreBase with Store {
|
|||
Future<void> initVideo() async {
|
||||
// Add event listeners to notify the JavaScript channels when the video plays and pauses.
|
||||
try {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("pause", () => VideoPause.postMessage("video paused"));');
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("playing", () => VideoPlaying.postMessage("video playing"));');
|
||||
controller?.runJavascript(
|
||||
'document.getElementsByTagName("video")[0].addEventListener("pause", () => VideoPause.postMessage("video paused"));');
|
||||
controller?.runJavascript(
|
||||
'document.getElementsByTagName("video")[0].addEventListener("playing", () => VideoPlaying.postMessage("video playing"));');
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
|
|
@ -186,17 +173,6 @@ abstract class VideoStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handles toggling "minimal mode" on the overlay.
|
||||
///
|
||||
/// "Minimal mode" is when only the channel name is visible on the bottom-left of the overlay.
|
||||
@action
|
||||
void handleExpand() {
|
||||
settingsStore.expandInfo = !settingsStore.expandInfo;
|
||||
|
||||
_overlayTimer.cancel();
|
||||
_overlayTimer = Timer(const Duration(seconds: 5), () => _overlayVisible = false);
|
||||
}
|
||||
|
||||
/// Handles the toggle overlay options.
|
||||
///
|
||||
/// The toggle overlay option allows switching between the custom and Twitch's overlay by long-pressing the overlay.
|
||||
|
|
@ -224,47 +200,6 @@ abstract class VideoStoreBase with Store {
|
|||
updateStreamInfo();
|
||||
}
|
||||
|
||||
/// Updates the sleep timer with [sleepHours] and [sleepMinutes].
|
||||
/// Calls [onTimerFinished] when the sleep timer completes.
|
||||
@action
|
||||
void updateSleepTimer({required void Function() onTimerFinished}) {
|
||||
// If hours and minutes are 0, do nothing.
|
||||
if (sleepHours == 0 && sleepMinutes == 0) return;
|
||||
|
||||
// If there is an ongoing timer, cancel it since it'll be replaced.
|
||||
if (sleepTimer != null) cancelSleepTimer();
|
||||
|
||||
// Update the new time remaining
|
||||
timeRemaining = Duration(hours: sleepHours, minutes: sleepMinutes);
|
||||
|
||||
// Reset the hours and minutes in the dropdown buttons.
|
||||
sleepHours = 0;
|
||||
sleepMinutes = 0;
|
||||
|
||||
// Set a periodic timer that will update the time remaining every second.
|
||||
sleepTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(timer) {
|
||||
// If the timer is up, cancel the timer and exit the app.
|
||||
if (timeRemaining.inSeconds == 0) {
|
||||
timer.cancel();
|
||||
onTimerFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrement the time remaining.
|
||||
timeRemaining = Duration(seconds: timeRemaining.inSeconds - 1);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Cancels the sleep timer and resets the time remaining.
|
||||
@action
|
||||
void cancelSleepTimer() {
|
||||
sleepTimer?.cancel();
|
||||
timeRemaining = const Duration();
|
||||
}
|
||||
|
||||
/// Play or pause the video depending on the current state of [_paused].
|
||||
void handlePausePlay() {
|
||||
try {
|
||||
|
|
@ -304,6 +239,5 @@ abstract class VideoStoreBase with Store {
|
|||
if (Platform.isIOS) controller?.reload();
|
||||
|
||||
_disposeOverlayReaction();
|
||||
sleepTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,54 +17,6 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
name: 'VideoStoreBase.videoUrl'))
|
||||
.value;
|
||||
|
||||
late final _$sleepHoursAtom =
|
||||
Atom(name: 'VideoStoreBase.sleepHours', context: context);
|
||||
|
||||
@override
|
||||
int get sleepHours {
|
||||
_$sleepHoursAtom.reportRead();
|
||||
return super.sleepHours;
|
||||
}
|
||||
|
||||
@override
|
||||
set sleepHours(int value) {
|
||||
_$sleepHoursAtom.reportWrite(value, super.sleepHours, () {
|
||||
super.sleepHours = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$sleepMinutesAtom =
|
||||
Atom(name: 'VideoStoreBase.sleepMinutes', context: context);
|
||||
|
||||
@override
|
||||
int get sleepMinutes {
|
||||
_$sleepMinutesAtom.reportRead();
|
||||
return super.sleepMinutes;
|
||||
}
|
||||
|
||||
@override
|
||||
set sleepMinutes(int value) {
|
||||
_$sleepMinutesAtom.reportWrite(value, super.sleepMinutes, () {
|
||||
super.sleepMinutes = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$timeRemainingAtom =
|
||||
Atom(name: 'VideoStoreBase.timeRemaining', context: context);
|
||||
|
||||
@override
|
||||
Duration get timeRemaining {
|
||||
_$timeRemainingAtom.reportRead();
|
||||
return super.timeRemaining;
|
||||
}
|
||||
|
||||
@override
|
||||
set timeRemaining(Duration value) {
|
||||
_$timeRemainingAtom.reportWrite(value, super.timeRemaining, () {
|
||||
super.timeRemaining = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_pausedAtom =
|
||||
Atom(name: 'VideoStoreBase._paused', context: context);
|
||||
|
||||
|
|
@ -167,17 +119,6 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleExpand() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.handleExpand');
|
||||
try {
|
||||
return super.handleExpand();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleToggleOverlay() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
|
|
@ -200,28 +141,6 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSleepTimer({required void Function() onTimerFinished}) {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.updateSleepTimer');
|
||||
try {
|
||||
return super.updateSleepTimer(onTimerFinished: onTimerFinished);
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void cancelSleepTimer() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.cancelSleepTimer');
|
||||
try {
|
||||
return super.cancelSleepTimer();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
|
|
@ -236,9 +155,6 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
sleepHours: ${sleepHours},
|
||||
sleepMinutes: ${sleepMinutes},
|
||||
timeRemaining: ${timeRemaining},
|
||||
videoUrl: ${videoUrl}
|
||||
''';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class _HomeState extends State<Home> {
|
|||
actions: [
|
||||
IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: const Icon(Icons.settings),
|
||||
icon: const Icon(Icons.settings_rounded),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -104,30 +104,23 @@ class _HomeState extends State<Home> {
|
|||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) => IndexedStack(
|
||||
index: _homeStore.selectedIndex,
|
||||
children: [
|
||||
if (_authStore.isLoggedIn)
|
||||
StreamsList(
|
||||
listType: ListType.followed,
|
||||
scrollController: _homeStore.followedScrollController,
|
||||
),
|
||||
TopSection(
|
||||
homeStore: _homeStore,
|
||||
),
|
||||
Search(
|
||||
scrollController: _homeStore.searchScrollController,
|
||||
),
|
||||
],
|
||||
child: Observer(
|
||||
builder: (_) => IndexedStack(
|
||||
index: _homeStore.selectedIndex,
|
||||
children: [
|
||||
if (_authStore.isLoggedIn)
|
||||
StreamsList(
|
||||
listType: ListType.followed,
|
||||
scrollController: _homeStore.followedScrollController,
|
||||
),
|
||||
TopSection(
|
||||
homeStore: _homeStore,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1.0, thickness: 1.0),
|
||||
],
|
||||
Search(
|
||||
scrollController: _homeStore.searchScrollController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Observer(
|
||||
|
|
@ -135,20 +128,24 @@ class _HomeState extends State<Home> {
|
|||
unselectedFontSize: 12.0,
|
||||
selectedFontSize: 12.0,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500),
|
||||
items: [
|
||||
if (_authStore.isLoggedIn)
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.favorite),
|
||||
BottomNavigationBarItem(
|
||||
icon: _homeStore.selectedIndex == 0
|
||||
? const Icon(Icons.favorite_rounded)
|
||||
: const Icon(Icons.favorite_border_rounded),
|
||||
label: 'Following',
|
||||
tooltip: 'Followed streams',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.arrow_upward),
|
||||
icon: Icon(Icons.arrow_upward_rounded),
|
||||
label: 'Top',
|
||||
tooltip: 'Top streams and categories',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.search),
|
||||
icon: Icon(Icons.search_rounded),
|
||||
label: 'Search',
|
||||
tooltip: 'Search for channels and categories',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -51,42 +51,42 @@ abstract class HomeStoreBase with Store {
|
|||
|
||||
// Use different logic if logged in/out since there will be one less tab when logged out.
|
||||
if (authStore.isLoggedIn) {
|
||||
if (index == 0) {
|
||||
if (index == 0 && followedScrollController.hasClients) {
|
||||
// If on the followed tab and tapping the followed tab, scroll to the top.
|
||||
followedScrollController.animateTo(
|
||||
0.0,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
} else if (index == 1) {
|
||||
} else if (index == 1 && topSectionScrollControllers[topSectionCurrentIndex].hasClients) {
|
||||
// If on the top section, scroll to the top of the tab based on the current top tab.
|
||||
topSectionScrollControllers[topSectionCurrentIndex].animateTo(
|
||||
0.0,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
} else {
|
||||
} else if (searchScrollController.hasClients) {
|
||||
// If on the search tab and tapping the search tab, scroll to the top.
|
||||
searchScrollController.animateTo(
|
||||
0.0,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (index == 0) {
|
||||
if (index == 0 && topSectionScrollControllers[topSectionCurrentIndex].hasClients) {
|
||||
// If on the top section, scroll to the top of the tab based on the current top tab.
|
||||
topSectionScrollControllers[topSectionCurrentIndex].animateTo(
|
||||
0.0,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
} else {
|
||||
} else if (searchScrollController.hasClients) {
|
||||
// If on the search tab and tapping the search tab, scroll to the top.
|
||||
searchScrollController.animateTo(
|
||||
0.0,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:frosty/screens/home/search/search_results_channels.dart';
|
|||
import 'package:frosty/screens/home/search/search_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
|
@ -45,12 +46,12 @@ class _SearchState extends State<Search> {
|
|||
focusNode: _searchStore.textFieldFocusNode,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.all(0.0),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
prefixIcon: const Icon(Icons.search_rounded),
|
||||
hintText: 'Find a channel or category',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchStore.textFieldFocusNode.hasFocus || _searchStore.searchText.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
tooltip: _searchStore.searchText.isEmpty ? 'Cancel' : 'Clear',
|
||||
onPressed: () {
|
||||
if (_searchStore.searchText.isEmpty) _searchStore.textFieldFocusNode.unfocus();
|
||||
|
|
@ -69,10 +70,7 @@ class _SearchState extends State<Search> {
|
|||
builder: (context) {
|
||||
if (_searchStore.textEditingController.text.isEmpty) {
|
||||
if (_searchStore.searchHistory.isEmpty) {
|
||||
return const AlertMessage(
|
||||
message: 'No recent searches',
|
||||
icon: Icons.search_off,
|
||||
);
|
||||
return const AlertMessage(message: 'No recent searches');
|
||||
}
|
||||
|
||||
return Column(
|
||||
|
|
@ -87,19 +85,22 @@ class _SearchState extends State<Search> {
|
|||
controller: widget.scrollController,
|
||||
children: _searchStore.searchHistory
|
||||
.mapIndexed(
|
||||
(index, searchTerm) => ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
title: Text(searchTerm),
|
||||
trailing: IconButton(
|
||||
tooltip: 'Remove',
|
||||
icon: const Icon(Icons.cancel),
|
||||
onPressed: () => _searchStore.searchHistory.removeAt(index),
|
||||
(index, searchTerm) => FrostyListTile(
|
||||
leading: const Icon(Icons.history_rounded),
|
||||
title: searchTerm,
|
||||
trailing: Tooltip(
|
||||
message: 'Remove',
|
||||
preferBelow: false,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () => _searchStore.searchHistory.removeAt(index),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
_searchStore.textEditingController.text = searchTerm;
|
||||
_searchStore.handleQuery(searchTerm);
|
||||
_searchStore.textEditingController.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: _searchStore.textEditingController.text.length));
|
||||
_searchStore.textEditingController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _searchStore.textEditingController.text.length));
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,10 +32,7 @@ class SearchResultsCategories extends StatelessWidget {
|
|||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: AlertMessage(
|
||||
message: 'Failed to get categories',
|
||||
icon: Icons.error,
|
||||
),
|
||||
child: AlertMessage(message: 'Failed to get categories'),
|
||||
),
|
||||
);
|
||||
case FutureStatus.fulfilled:
|
||||
|
|
@ -45,10 +42,7 @@ class SearchResultsCategories extends StatelessWidget {
|
|||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: AlertMessage(
|
||||
message: 'Failed to get categories',
|
||||
icon: Icons.error,
|
||||
),
|
||||
child: AlertMessage(message: 'Failed to get categories'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import 'package:frosty/screens/home/search/search_store.dart';
|
|||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/animate_scale.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:frosty/widgets/uptime.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class SearchResultsChannels extends StatefulWidget {
|
||||
|
|
@ -44,10 +46,7 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
);
|
||||
} catch (error) {
|
||||
final snackBar = SnackBar(
|
||||
content: AlertMessage(
|
||||
message: error.toString(),
|
||||
icon: Icons.error,
|
||||
),
|
||||
content: AlertMessage(message: error.toString()),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
|
|
@ -72,79 +71,77 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 100.0,
|
||||
child: AlertMessage(
|
||||
message: 'Failed to get channels',
|
||||
icon: Icons.error,
|
||||
),
|
||||
child: AlertMessage(message: 'Failed to get channels'),
|
||||
),
|
||||
);
|
||||
case FutureStatus.fulfilled:
|
||||
final results = (future.result as List<ChannelQuery>)
|
||||
.where((channel) => !widget.searchStore.authStore.user.blockedUsers.map((blockedUser) => blockedUser.userId).contains(channel.id));
|
||||
final results = (future.result as List<ChannelQuery>).where((channel) => !widget
|
||||
.searchStore.authStore.user.blockedUsers
|
||||
.map((blockedUser) => blockedUser.userId)
|
||||
.contains(channel.id));
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate.fixed([
|
||||
...results.map(
|
||||
(channel) {
|
||||
final displayName =
|
||||
regexEnglish.hasMatch(channel.displayName) ? channel.displayName : '${channel.displayName} (${channel.broadcasterLogin})';
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
[
|
||||
...results.map(
|
||||
(channel) {
|
||||
final displayName = regexEnglish.hasMatch(channel.displayName)
|
||||
? channel.displayName
|
||||
: '${channel.displayName} (${channel.broadcasterLogin})';
|
||||
|
||||
return AnimateScale(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VideoChat(
|
||||
userId: channel.id,
|
||||
userName: channel.displayName,
|
||||
userLogin: channel.broadcasterLogin,
|
||||
return AnimateScale(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VideoChat(
|
||||
userId: channel.id,
|
||||
userName: channel.displayName,
|
||||
userLogin: channel.broadcasterLogin,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onLongPress: () {
|
||||
HapticFeedback.lightImpact();
|
||||
onLongPress: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => BlockReportModal(
|
||||
authStore: widget.searchStore.authStore,
|
||||
name: displayName,
|
||||
userLogin: channel.broadcasterLogin,
|
||||
userId: channel.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(displayName),
|
||||
leading: ProfilePicture(userLogin: channel.broadcasterLogin),
|
||||
trailing: channel.isLive
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
child: Container(
|
||||
color: const Color(0xFFF44336),
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: const Text(
|
||||
'LIVE',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => BlockReportModal(
|
||||
authStore: widget.searchStore.authStore,
|
||||
name: displayName,
|
||||
userLogin: channel.broadcasterLogin,
|
||||
userId: channel.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FrostyListTile(
|
||||
isThreeLine: false,
|
||||
title: displayName,
|
||||
leading: ProfilePicture(userLogin: channel.broadcasterLogin),
|
||||
subtitle: channel.isLive
|
||||
? Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.circle,
|
||||
color: Colors.red,
|
||||
size: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
subtitle:
|
||||
channel.isLive ? Text('Uptime: ${DateTime.now().difference(DateTime.parse(channel.startedAt)).toString().split('.')[0]}') : null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Go to channel "${widget.query}"'),
|
||||
onTap: () => _handleSearch(context, widget.query),
|
||||
trailing: Icon(Icons.adaptive.arrow_forward),
|
||||
)
|
||||
]),
|
||||
const SizedBox(width: 5),
|
||||
Uptime(startTime: channel.startedAt),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
FrostyListTile(
|
||||
title: 'Go to channel "${widget.query}"',
|
||||
onTap: () => _handleSearch(context, widget.query),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
172
lib/screens/home/stream_list/large_stream_card.dart
Normal file
172
lib/screens/home/stream_list/large_stream_card.dart
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/screens/channel/channel.dart';
|
||||
import 'package:frosty/screens/channel/video/video_bar.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/animate_scale.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/uptime.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LargeStreamCard extends StatelessWidget {
|
||||
final StreamTwitch streamInfo;
|
||||
final bool showThumbnail;
|
||||
final bool showCategory;
|
||||
|
||||
const LargeStreamCard({
|
||||
Key? key,
|
||||
required this.streamInfo,
|
||||
required this.showThumbnail,
|
||||
this.showCategory = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get a new URL for the thumbnail every 5 minutes so that the image is updated on refresh.
|
||||
// This method adds a random value to the end of the URL to override the cached image.
|
||||
final time = DateTime.now();
|
||||
final cacheUrlExtension = time.day.toString() + time.hour.toString() + (time.minute ~/ 5).toString();
|
||||
|
||||
// Calculate the width and height of the thumbnail based on the device width and the stream card size setting.
|
||||
// Constraint the resolution to 1920x1080 since that's the max resolution of the Twitch API.
|
||||
final size = MediaQuery.of(context).size;
|
||||
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final thumbnailWidth = min((size.width * pixelRatio) ~/ 1, 1920);
|
||||
final thumbnailHeight = min((thumbnailWidth * (9 / 16)).toInt(), 1080);
|
||||
|
||||
final thumbnail = ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
children: [
|
||||
Container(
|
||||
foregroundDecoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: [0.8, 1.0],
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl:
|
||||
streamInfo.thumbnailUrl.replaceFirst('-{width}x{height}', '-${thumbnailWidth}x$thumbnailHeight') +
|
||||
cacheUrlExtension,
|
||||
placeholder: (context, url) => const ColoredBox(color: lightGray, child: LoadingIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, bottom: 12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: 'Stream uptime',
|
||||
preferBelow: false,
|
||||
child: Row(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(2.0),
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
color: Colors.red,
|
||||
size: 10,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3.0),
|
||||
Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10.0),
|
||||
Tooltip(
|
||||
message: 'Viewer count',
|
||||
preferBelow: false,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.visibility,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
NumberFormat().format(streamInfo.viewerCount),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName)
|
||||
? streamInfo.userName
|
||||
: '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
|
||||
return AnimateScale(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VideoChat(
|
||||
userId: streamInfo.userId,
|
||||
userName: streamInfo.userName,
|
||||
userLogin: streamInfo.userLogin,
|
||||
),
|
||||
),
|
||||
),
|
||||
onLongPress: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => BlockReportModal(
|
||||
authStore: context.read<AuthStore>(),
|
||||
name: streamerName,
|
||||
userLogin: streamInfo.userLogin,
|
||||
userId: streamInfo.userId,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: showThumbnail ? 10.0 : 5.0, horizontal: 15.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (showThumbnail) thumbnail,
|
||||
VideoBar(
|
||||
streamInfo: streamInfo,
|
||||
showCategory: showCategory,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/models/stream.dart';
|
||||
import 'package:frosty/screens/channel/channel.dart';
|
||||
import 'package:frosty/screens/home/top/categories/category_streams.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/animate_scale.dart';
|
||||
import 'package:frosty/widgets/block_report_modal.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:frosty/widgets/uptime.dart';
|
||||
|
|
@ -19,17 +20,13 @@ import 'package:provider/provider.dart';
|
|||
/// A tappable card widget that displays a stream's thumbnail and details.
|
||||
class StreamCard extends StatelessWidget {
|
||||
final StreamTwitch streamInfo;
|
||||
final bool showUptime;
|
||||
final bool showThumbnail;
|
||||
final bool large;
|
||||
final bool showCategory;
|
||||
|
||||
const StreamCard({
|
||||
Key? key,
|
||||
required this.streamInfo,
|
||||
required this.showUptime,
|
||||
required this.showThumbnail,
|
||||
required this.large,
|
||||
this.showCategory = true,
|
||||
}) : super(key: key);
|
||||
|
||||
|
|
@ -44,84 +41,55 @@ class StreamCard extends StatelessWidget {
|
|||
// Constraint the resolution to 1920x1080 since that's the max resolution of the Twitch API.
|
||||
final size = MediaQuery.of(context).size;
|
||||
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
final thumbnailWidth = min((size.width * pixelRatio) ~/ (large ? 1 : 3), 1920);
|
||||
final thumbnailWidth = min((size.width * pixelRatio) ~/ 3, 1920);
|
||||
final thumbnailHeight = min((thumbnailWidth * (9 / 16)).toInt(), 1080);
|
||||
|
||||
final image = AspectRatio(
|
||||
final thumbnail = AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: streamInfo.thumbnailUrl.replaceFirst('-{width}x{height}', '-${thumbnailWidth}x$thumbnailHeight') + cacheUrlExtension,
|
||||
placeholder: (context, url) => const LoadingIndicator(),
|
||||
useOldImageOnUrlChange: true,
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: streamInfo.thumbnailUrl.replaceFirst('-{width}x{height}', '-${thumbnailWidth}x$thumbnailHeight') +
|
||||
cacheUrlExtension,
|
||||
placeholder: (context, url) => const ColoredBox(color: lightGray, child: LoadingIndicator()),
|
||||
),
|
||||
);
|
||||
|
||||
final thumbnail = large
|
||||
? Container(
|
||||
foregroundDecoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: image,
|
||||
)
|
||||
: image;
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName)
|
||||
? streamInfo.userName
|
||||
: '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName) ? streamInfo.userName : '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
const subFontSize = 14.0;
|
||||
|
||||
final subFontSize = large ? 16.0 : 14.0;
|
||||
|
||||
final fontColor = large ? Colors.white : DefaultTextStyle.of(context).style.color;
|
||||
final fontColor = DefaultTextStyle.of(context).style.color;
|
||||
|
||||
final imageSection = ClipRRect(
|
||||
borderRadius: large ? const BorderRadius.all(Radius.circular(10.0)) : const BorderRadius.all(Radius.circular(5.0)),
|
||||
child: showUptime
|
||||
? Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
thumbnail,
|
||||
if (large)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromRGBO(0, 0, 0, 0.5),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(3.0),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.all(2.0),
|
||||
child: Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
children: [
|
||||
thumbnail,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color.fromRGBO(0, 0, 0, 0.5),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(3.0),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.all(2.0),
|
||||
child: Uptime(
|
||||
startTime: streamInfo.startedAt,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: thumbnail,
|
||||
);
|
||||
],
|
||||
));
|
||||
|
||||
final streamInfoSection = Padding(
|
||||
padding: large ? const EdgeInsets.all(10.0) : const EdgeInsets.only(left: 10.0),
|
||||
padding: const EdgeInsets.only(left: 10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -134,16 +102,16 @@ class StreamCard extends StatelessWidget {
|
|||
const SizedBox(width: 5.0),
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
message: streamerName,
|
||||
message: 'Streamer: $streamerName',
|
||||
preferBelow: false,
|
||||
child: Text(
|
||||
streamerName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: large ? 20.0 : 16.0,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: fontColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -151,7 +119,7 @@ class StreamCard extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Tooltip(
|
||||
message: streamInfo.title.trim(),
|
||||
message: 'Title: ${streamInfo.title.trim()}',
|
||||
preferBelow: false,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Text(
|
||||
|
|
@ -178,7 +146,7 @@ class StreamCard extends StatelessWidget {
|
|||
)
|
||||
: null,
|
||||
child: Tooltip(
|
||||
message: streamInfo.gameName,
|
||||
message: 'Category: ${streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'None'}',
|
||||
preferBelow: false,
|
||||
child: Text(
|
||||
streamInfo.gameName.isNotEmpty ? streamInfo.gameName : 'No Category',
|
||||
|
|
@ -230,24 +198,19 @@ class StreamCard extends StatelessWidget {
|
|||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: showThumbnail ? 15.0 : 5.0),
|
||||
child: large
|
||||
? Stack(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
children: [if (showThumbnail) imageSection, streamInfoSection],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
if (showThumbnail)
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: imageSection,
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: streamInfoSection,
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
if (showThumbnail)
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: imageSection,
|
||||
),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: streamInfoSection,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/screens/home/stream_list/large_stream_card.dart';
|
||||
import 'package:frosty/screens/home/stream_list/stream_card.dart';
|
||||
import 'package:frosty/screens/home/stream_list/stream_list_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
|
|
@ -66,10 +67,7 @@ class _StreamsListState extends State<StreamsList> with AutomaticKeepAliveClient
|
|||
|
||||
if (_listStore.error != null) {
|
||||
final snackBar = SnackBar(
|
||||
content: AlertMessage(
|
||||
message: _listStore.error!,
|
||||
icon: Icons.error,
|
||||
),
|
||||
content: AlertMessage(message: _listStore.error!),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
|
|
@ -82,17 +80,15 @@ class _StreamsListState extends State<StreamsList> with AutomaticKeepAliveClient
|
|||
Widget? statusWidget;
|
||||
|
||||
if (_listStore.error != null) {
|
||||
statusWidget = AlertMessage(
|
||||
message: _listStore.error!,
|
||||
icon: Icons.error,
|
||||
);
|
||||
statusWidget = AlertMessage(message: _listStore.error!);
|
||||
}
|
||||
|
||||
if (_listStore.streams.isEmpty) {
|
||||
if (_listStore.isLoading && _listStore.error == null) {
|
||||
statusWidget = const LoadingIndicator(subtitle: 'Loading streams...');
|
||||
} else {
|
||||
statusWidget = AlertMessage(message: widget.listType == ListType.followed ? 'No followed streams' : 'No top streams');
|
||||
statusWidget = AlertMessage(
|
||||
message: widget.listType == ListType.followed ? 'No followed streams' : 'No top streams');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,12 +115,17 @@ class _StreamsListState extends State<StreamsList> with AutomaticKeepAliveClient
|
|||
_listStore.getStreams();
|
||||
}
|
||||
return Observer(
|
||||
builder: (context) => StreamCard(
|
||||
streamInfo: _listStore.streams[index],
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
large: context.read<SettingsStore>().largeStreamCard,
|
||||
showUptime: context.read<SettingsStore>().showThumbnailUptime,
|
||||
),
|
||||
builder: (context) => context.read<SettingsStore>().largeStreamCard
|
||||
? LargeStreamCard(
|
||||
key: ValueKey(_listStore.streams[index].userId),
|
||||
streamInfo: _listStore.streams[index],
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
)
|
||||
: StreamCard(
|
||||
key: ValueKey(_listStore.streams[index].userId),
|
||||
streamInfo: _listStore.streams[index],
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -56,10 +56,7 @@ class _CategoriesState extends State<Categories> with AutomaticKeepAliveClientMi
|
|||
|
||||
if (_categoriesStore.error != null) {
|
||||
final snackBar = SnackBar(
|
||||
content: AlertMessage(
|
||||
message: _categoriesStore.error!,
|
||||
icon: Icons.error,
|
||||
),
|
||||
content: AlertMessage(message: _categoriesStore.error!),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
|
|
@ -72,10 +69,7 @@ class _CategoriesState extends State<Categories> with AutomaticKeepAliveClientMi
|
|||
Widget? statusWidget;
|
||||
|
||||
if (_categoriesStore.error != null) {
|
||||
statusWidget = AlertMessage(
|
||||
message: _categoriesStore.error!,
|
||||
icon: Icons.error,
|
||||
);
|
||||
statusWidget = AlertMessage(message: _categoriesStore.error!);
|
||||
}
|
||||
|
||||
if (_categoriesStore.categories.isEmpty) {
|
||||
|
|
@ -110,6 +104,7 @@ class _CategoriesState extends State<Categories> with AutomaticKeepAliveClientMi
|
|||
_categoriesStore.getCategories();
|
||||
}
|
||||
return CategoryCard(
|
||||
key: ValueKey(_categoriesStore.categories[index].id),
|
||||
category: _categoriesStore.categories[index],
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/models/category.dart';
|
||||
import 'package:frosty/screens/home/top/categories/category_streams.dart';
|
||||
import 'package:frosty/widgets/animate_scale.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
|
||||
/// A tappable card widget that displays a category's box art and name under.
|
||||
|
|
@ -22,7 +22,7 @@ class CategoryCard extends StatelessWidget {
|
|||
final artWidth = (size.width * pixelRatio) ~/ 3;
|
||||
final artHeight = (artWidth * (4 / 3)).toInt();
|
||||
|
||||
return AnimateScale(
|
||||
return InkWell(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -38,12 +38,13 @@ class CategoryCard extends StatelessWidget {
|
|||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: category.boxArtUrl.replaceRange(category.boxArtUrl.lastIndexOf('-') + 1, null, '${artWidth}x$artHeight.jpg'),
|
||||
placeholder: (context, url) => const LoadingIndicator(),
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: category.boxArtUrl
|
||||
.replaceRange(category.boxArtUrl.lastIndexOf('-') + 1, null, '${artWidth}x$artHeight.jpg'),
|
||||
placeholder: (context, url) => const ColoredBox(color: lightGray, child: LoadingIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/models/category.dart';
|
||||
import 'package:frosty/screens/home/stream_list/large_stream_card.dart';
|
||||
import 'package:frosty/screens/home/stream_list/stream_card.dart';
|
||||
import 'package:frosty/screens/home/stream_list/stream_list_store.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/cached_image.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
@ -57,10 +59,7 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
|
||||
if (_listStore.error != null) {
|
||||
final snackBar = SnackBar(
|
||||
content: AlertMessage(
|
||||
message: _listStore.error!,
|
||||
icon: Icons.error,
|
||||
),
|
||||
content: AlertMessage(message: _listStore.error!),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
|
|
@ -78,6 +77,10 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
controller: _listStore.scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.adaptive.arrow_back_rounded),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
stretch: true,
|
||||
pinned: true,
|
||||
expandedHeight: MediaQuery.of(context).size.height / 3,
|
||||
|
|
@ -100,13 +103,14 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
),
|
||||
builder: (context, AsyncSnapshot<CategoriesTwitch> snapshot) {
|
||||
return snapshot.hasData
|
||||
? CachedNetworkImage(
|
||||
? FrostyCachedNetworkImage(
|
||||
imageUrl: snapshot.data!.data.first.boxArtUrl.replaceRange(
|
||||
snapshot.data!.data.first.boxArtUrl.lastIndexOf('-') + 1,
|
||||
null,
|
||||
'${artWidth}x$artHeight.jpg',
|
||||
),
|
||||
placeholder: (context, url) => const LoadingIndicator(),
|
||||
placeholder: (context, url) =>
|
||||
const ColoredBox(color: lightGray, child: LoadingIndicator()),
|
||||
color: const Color.fromRGBO(255, 255, 255, 0.5),
|
||||
colorBlendMode: BlendMode.modulate,
|
||||
fit: BoxFit.cover,
|
||||
|
|
@ -119,10 +123,7 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
if (_listStore.error != null)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: AlertMessage(
|
||||
message: _listStore.error!,
|
||||
icon: Icons.error,
|
||||
),
|
||||
child: AlertMessage(message: _listStore.error!),
|
||||
)
|
||||
else if (_listStore.streams.isEmpty)
|
||||
if (_listStore.isLoading && _listStore.error == null)
|
||||
|
|
@ -149,13 +150,17 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
_listStore.getStreams();
|
||||
}
|
||||
return Observer(
|
||||
builder: (context) => StreamCard(
|
||||
streamInfo: _listStore.streams[index],
|
||||
showUptime: context.read<SettingsStore>().showThumbnailUptime,
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
large: context.read<SettingsStore>().largeStreamCard,
|
||||
showCategory: false,
|
||||
),
|
||||
builder: (context) => context.read<SettingsStore>().largeStreamCard
|
||||
? LargeStreamCard(
|
||||
streamInfo: _listStore.streams[index],
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
showCategory: false,
|
||||
)
|
||||
: StreamCard(
|
||||
streamInfo: _listStore.streams[index],
|
||||
showThumbnail: context.read<SettingsStore>().showThumbnails,
|
||||
showCategory: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: _listStore.streams.length,
|
||||
|
|
@ -168,9 +173,11 @@ class _CategoryStreamsState extends State<CategoryStreams> {
|
|||
child: Observer(
|
||||
builder: (context) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
child: _listStore.showJumpButton ? ScrollToTopButton(scrollController: _listStore.scrollController!) : null,
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: _listStore.showJumpButton
|
||||
? ScrollToTopButton(scrollController: _listStore.scrollController!)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/main.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';
|
||||
|
|
@ -28,23 +29,19 @@ class _TopSectionState extends State<TopSection> with SingleTickerProviderStateM
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TabBar(
|
||||
controller: _tabBarController,
|
||||
indicatorColor: const Color(0xff9146ff),
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicatorColor: purple,
|
||||
tabs: const [
|
||||
Tab(
|
||||
child: Text(
|
||||
'Streams',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'Categories',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Tab(child: Text('Streams')),
|
||||
Tab(child: Text('Categories')),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
|
|
|
|||
59
lib/screens/settings/account/account_options.dart
Normal file
59
lib/screens/settings/account/account_options.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/screens/settings/account/blocked_users.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_tile_route.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
|
||||
class AccountOptions extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const AccountOptions({Key? key, required this.authStore}) : super(key: key);
|
||||
|
||||
Future<void> _showLogoutDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Log Out',
|
||||
message: 'Are you sure you want to log out?',
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
authStore.logout();
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.grey,
|
||||
child: const Text('Cancel'),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
children: [
|
||||
SettingsTileRoute(
|
||||
leading: const Icon(Icons.block_rounded),
|
||||
title: 'Blocked',
|
||||
child: BlockedUsers(
|
||||
authStore: authStore,
|
||||
),
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.logout_rounded),
|
||||
title: 'Log out',
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () => _showLogoutDialog(context),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/screens/settings/account/blocked_users.dart
Normal file
61
lib/screens/settings/account/blocked_users.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
|
||||
class BlockedUsers extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const BlockedUsers({
|
||||
Key? key,
|
||||
required this.authStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
await authStore.user.refreshBlockedUsers(headers: authStore.headersTwitch);
|
||||
},
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (authStore.user.blockedUsers.isEmpty) {
|
||||
return const Center(
|
||||
child: AlertMessage(
|
||||
message: 'No blocked users',
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
children: authStore.user.blockedUsers.map(
|
||||
(blockedUser) {
|
||||
final displayName = regexEnglish.hasMatch(blockedUser.displayName)
|
||||
? blockedUser.displayName
|
||||
: '${blockedUser.displayName} (${blockedUser.userLogin})';
|
||||
|
||||
return FrostyListTile(
|
||||
title: displayName,
|
||||
trailing: Button(
|
||||
color: Colors.red,
|
||||
onPressed: () => authStore.showBlockDialog(
|
||||
context,
|
||||
targetUser: displayName,
|
||||
targetUserId: blockedUser.userId,
|
||||
),
|
||||
child: const Text('Unblock'),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
121
lib/screens/settings/account/widgets/profile_card.dart
Normal file
121
lib/screens/settings/account/widgets/profile_card.dart
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/account/account_options.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_tile_route.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:simple_icons/simple_icons.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class ProfileCard extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const ProfileCard({Key? key, required this.authStore}) : super(key: key);
|
||||
|
||||
Future<void> _showLoginDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Log In',
|
||||
content: Column(
|
||||
children: [
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 15.0),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: const FrostyAppBar(
|
||||
title: Text('Connect with Twitch'),
|
||||
),
|
||||
body: WebView(
|
||||
initialUrl: authStore.loginUri.toString(),
|
||||
navigationDelegate: authStore.handleNavigation,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
icon: const Icon(SimpleIcons.twitch),
|
||||
child: const Text(
|
||||
'Connect with Twitch',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Or',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Token',
|
||||
),
|
||||
onSubmitted: (token) {
|
||||
authStore.login(token: token);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
if (authStore.error != null) {
|
||||
return FrostyListTile(
|
||||
leading: const Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
title: 'Failed to connect',
|
||||
trailing: Button(
|
||||
onPressed: authStore.init,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
child: const Text('Reconnect'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (authStore.isLoggedIn && authStore.user.details != null) {
|
||||
return SettingsTileRoute(
|
||||
leading: ProfilePicture(
|
||||
userLogin: authStore.user.details!.login,
|
||||
radius: 12,
|
||||
),
|
||||
title: authStore.user.details!.displayName,
|
||||
child: AccountOptions(authStore: authStore),
|
||||
);
|
||||
}
|
||||
return FrostyListTile(
|
||||
leading: const Icon(Icons.no_accounts_rounded),
|
||||
title: 'Anonymous',
|
||||
subtitle: const Text('Tap to log in and enable the ability to chat, view followed streams, and more.'),
|
||||
trailing: const SizedBox(
|
||||
height: double.infinity,
|
||||
child: Icon(Icons.chevron_right_rounded),
|
||||
),
|
||||
onTap: () => _showLoginDialog(context),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.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/block_button.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class AccountSettings extends StatelessWidget {
|
||||
final SettingsStore settingsStore;
|
||||
final AuthStore authStore;
|
||||
|
||||
const AccountSettings({
|
||||
Key? key,
|
||||
required this.settingsStore,
|
||||
required this.authStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) => ExpansionTile(
|
||||
leading: const Icon(Icons.account_circle),
|
||||
title: const Text(
|
||||
'Account',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
ProfileCard(authStore: authStore),
|
||||
if (authStore.isLoggedIn) ...[
|
||||
ListTile(
|
||||
title: const Text('Blocked users'),
|
||||
trailing: Icon(Icons.adaptive.arrow_forward),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlockedUsers(
|
||||
authStore: authStore,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileCard extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const ProfileCard({Key? key, required this.authStore}) : super(key: key);
|
||||
|
||||
Future<void> _showLoginDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Log In',
|
||||
content: Column(
|
||||
children: [
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 15.0),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connect with Twitch'),
|
||||
),
|
||||
body: WebView(
|
||||
initialUrl: authStore.loginUri.toString(),
|
||||
navigationDelegate: authStore.handleNavigation,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
icon: Image.asset(
|
||||
'assets/icons/TwitchGlitchWhite.png',
|
||||
height: 25,
|
||||
),
|
||||
child: const Text(
|
||||
'Connect with Twitch',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Or',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
textAlign: TextAlign.center,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Token',
|
||||
),
|
||||
onSubmitted: (token) {
|
||||
authStore.login(token: token);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showLogoutDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Log Out',
|
||||
content: const Text('Are you sure you want to log out?'),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
authStore.logout();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
fill: true,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.red.shade700,
|
||||
child: const Text('Cancel'),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
if (authStore.error != null) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.error),
|
||||
title: const Text('Failed to connect'),
|
||||
trailing: Button(
|
||||
onPressed: authStore.init,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
child: const Text('Try Again'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (authStore.isLoggedIn && authStore.user.details != null) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundImage: CachedNetworkImageProvider(authStore.user.details!.profileImageUrl),
|
||||
),
|
||||
title: Text(
|
||||
authStore.user.details!.displayName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
trailing: Button(
|
||||
onPressed: () => _showLogoutDialog(context),
|
||||
icon: const Icon(Icons.logout_outlined),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
fill: true,
|
||||
color: Colors.red.shade700,
|
||||
child: const Text('Log Out'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
leading: const Icon(
|
||||
Icons.no_accounts,
|
||||
size: 40,
|
||||
),
|
||||
title: const Text('Anonymous User'),
|
||||
subtitle: const Text('Log in to chat, view followed streams, and more.'),
|
||||
trailing: Button(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
onPressed: () => _showLoginDialog(context),
|
||||
fill: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
icon: const Icon(Icons.login),
|
||||
child: const Text('Log In'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BlockedUsers extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const BlockedUsers({
|
||||
Key? key,
|
||||
required this.authStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Blocked Users'),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
await authStore.user.refreshBlockedUsers(headers: authStore.headersTwitch);
|
||||
},
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (authStore.user.blockedUsers.isEmpty) {
|
||||
return const Center(
|
||||
child: AlertMessage(
|
||||
message: 'No blocked users',
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
children: authStore.user.blockedUsers.map(
|
||||
(user) {
|
||||
final displayName = regexEnglish.hasMatch(user.displayName) ? user.displayName : '${user.displayName} (${user.userLogin})';
|
||||
|
||||
return ListTile(
|
||||
title: Tooltip(
|
||||
preferBelow: false,
|
||||
message: displayName,
|
||||
child: Text(
|
||||
displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
trailing: BlockButton(
|
||||
authStore: authStore,
|
||||
targetUser: displayName,
|
||||
targetUserId: user.userId,
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_select.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_slider.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class ChatSettings extends StatefulWidget {
|
||||
|
|
@ -25,231 +28,18 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
|
||||
const timestamps = ['Disabled', '12-hour', '24-hour'];
|
||||
|
||||
const sectionPadding = EdgeInsets.only(left: 16.0, bottom: 5.0, top: 30.0);
|
||||
|
||||
final settingsStore = widget.settingsStore;
|
||||
|
||||
return Observer(
|
||||
builder: (context) => ExpansionTile(
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
childrenPadding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
leading: const Icon(Icons.chat),
|
||||
title: const Text(
|
||||
'Chat',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
builder: (context) => ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Message delay'),
|
||||
const Spacer(),
|
||||
Text('${settingsStore.chatDelay.toInt()} ${settingsStore.chatDelay == 1.0 ? 'second' : 'seconds'}'),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: settingsStore.chatDelay,
|
||||
min: 0.0,
|
||||
max: 30.0,
|
||||
divisions: 30,
|
||||
onChanged: (newValue) => settingsStore.chatDelay = newValue,
|
||||
),
|
||||
Text('Adds a delay before each message is rendered in chat. ${Platform.isIOS ? '15 seconds is recommended for iOS.' : ''}'),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Prevent sleep in chat-only mode'),
|
||||
subtitle: const Text('Requires restarting the chat in order to take effect.'),
|
||||
value: settingsStore.chatOnlyPreventSleep,
|
||||
onChanged: !settingsStore.showVideo ? (newValue) => settingsStore.chatOnlyPreventSleep = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Autocomplete'),
|
||||
subtitle: const Text('Shows a bar that suggests matching emotes and mentions while typing.'),
|
||||
value: settingsStore.autocomplete,
|
||||
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.autocomplete = newValue : null,
|
||||
),
|
||||
const SectionHeader(
|
||||
'Layout',
|
||||
padding: sectionPadding,
|
||||
showDivider: true,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Bottom bar'),
|
||||
value: settingsStore.showBottomBar,
|
||||
onChanged: (newValue) => settingsStore.showBottomBar = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Emote menu button on left side'),
|
||||
value: settingsStore.emoteMenuButtonOnLeft,
|
||||
onChanged: (newValue) => settingsStore.emoteMenuButtonOnLeft = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Landscape chat on left side'),
|
||||
value: settingsStore.landscapeChatLeftSide,
|
||||
onChanged: (newValue) => settingsStore.landscapeChatLeftSide = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Force vertical chat in landscape mode'),
|
||||
subtitle: const Text('Note: this option is intended for tablets and other larger displays.'),
|
||||
value: settingsStore.landscapeForceVerticalChat,
|
||||
onChanged: (newValue) => settingsStore.landscapeForceVerticalChat = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Notifications on bottom'),
|
||||
subtitle: const Text('Shows notifications (e.g., "Message copied") on the bottom of the chat.'),
|
||||
value: settingsStore.chatNotificationsOnBottom,
|
||||
onChanged: (newValue) => settingsStore.chatNotificationsOnBottom = newValue,
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: const Text('Landscape fill cutout side'),
|
||||
subtitle: const Text('Overrides and fills the available space in the display cutout/notch.'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.landscapeCutout,
|
||||
onChanged: (LandscapeCutoutType? newValue) => settingsStore.landscapeCutout = newValue!,
|
||||
items: LandscapeCutoutType.values
|
||||
.map((LandscapeCutoutType value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(landscapeCutouts[value.index]),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Chat width'),
|
||||
const Spacer(),
|
||||
Text('${(settingsStore.chatWidth * 100).toStringAsFixed(0)}%'),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: settingsStore.chatWidth,
|
||||
min: 0.2,
|
||||
max: 0.6,
|
||||
divisions: 8,
|
||||
onChanged: (newValue) => settingsStore.chatWidth = newValue,
|
||||
),
|
||||
const Text('Sets the width of the chat in fullscreen and theater mode.'),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Chat overlay opacity'),
|
||||
const Spacer(),
|
||||
Text('${(settingsStore.fullScreenChatOverlayOpacity * 100).toStringAsFixed(0)}%'),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: settingsStore.fullScreenChatOverlayOpacity,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
divisions: 10,
|
||||
onChanged: (newValue) => settingsStore.fullScreenChatOverlayOpacity = newValue,
|
||||
),
|
||||
const Text('Sets the opacity (transparency) of the overlay chat in fullscreen mode.'),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SectionHeader(
|
||||
'Message Appearance',
|
||||
padding: sectionPadding,
|
||||
showDivider: true,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Readable name colors'),
|
||||
subtitle: const Text('Adjusts the lightness value of overly bright or dark names.'),
|
||||
value: settingsStore.useReadableColors,
|
||||
onChanged: (newValue) => settingsStore.useReadableColors = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Show deleted messages'),
|
||||
subtitle: const Text('Restores the original message of deleted messages.'),
|
||||
value: settingsStore.showDeletedMessages,
|
||||
onChanged: (newValue) => settingsStore.showDeletedMessages = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Message dividers'),
|
||||
subtitle: const Text('Shows a subtle divider between each message.'),
|
||||
value: settingsStore.showChatMessageDividers,
|
||||
onChanged: (newValue) => settingsStore.showChatMessageDividers = newValue,
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: const Text('Message timestamps'),
|
||||
subtitle: const Text('Shows timestamps for when a message was sent.'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.timestampType,
|
||||
onChanged: (TimestampType? newTimestamp) => settingsStore.timestampType = newTimestamp!,
|
||||
items: TimestampType.values
|
||||
.map((TimestampType value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(timestamps[value.index]),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const SectionHeader(
|
||||
'Alerts',
|
||||
padding: sectionPadding,
|
||||
showDivider: true,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Highlight first time chatters'),
|
||||
value: settingsStore.highlightFirstTimeChatter,
|
||||
onChanged: (newValue) => settingsStore.highlightFirstTimeChatter = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Show notices'),
|
||||
subtitle: const Text('Shows notices such as subs and re-subs, announcements, and raids.'),
|
||||
value: settingsStore.showUserNotices,
|
||||
onChanged: (newValue) => settingsStore.showUserNotices = newValue,
|
||||
),
|
||||
const SectionHeader(
|
||||
'Message Sizing',
|
||||
padding: sectionPadding,
|
||||
showDivider: true,
|
||||
),
|
||||
const SectionHeader('Message sizing'),
|
||||
ExpansionTile(
|
||||
title: const Text('Preview'),
|
||||
title: const Text('Preview', style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.deepPurple),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: settingsStore.fontSize),
|
||||
child: Column(
|
||||
|
|
@ -260,8 +50,9 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: 'https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/3',
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl:
|
||||
'https://static-cdn.jtvnw.net/badges/v1/bbbe0db0-a598-423e-86d0-f9fb98ca1933/3',
|
||||
height: defaultBadgeSize * settingsStore.badgeScale,
|
||||
width: defaultBadgeSize * settingsStore.badgeScale,
|
||||
),
|
||||
|
|
@ -269,7 +60,7 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
const TextSpan(text: ' Badge and emote preview. '),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CachedNetworkImage(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: 'https://static-cdn.jtvnw.net/emoticons/v2/425618/default/dark/3.0',
|
||||
height: defaultEmoteSize * settingsStore.emoteScale,
|
||||
width: defaultEmoteSize * settingsStore.emoteScale,
|
||||
|
|
@ -295,86 +86,164 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Badge scale'),
|
||||
const Spacer(),
|
||||
Text('${settingsStore.badgeScale.toStringAsFixed(2)}x'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.badgeScale,
|
||||
min: 0.25,
|
||||
max: 3.0,
|
||||
divisions: 11,
|
||||
onChanged: (newValue) => settingsStore.badgeScale = newValue,
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Badge scale',
|
||||
trailing: '${settingsStore.badgeScale.toStringAsFixed(2)}x',
|
||||
value: settingsStore.badgeScale,
|
||||
min: 0.25,
|
||||
max: 3.0,
|
||||
divisions: 11,
|
||||
onChanged: (newValue) => settingsStore.badgeScale = newValue,
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Emote scale'),
|
||||
const Spacer(),
|
||||
Text('${settingsStore.emoteScale.toStringAsFixed(2)}x'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.emoteScale,
|
||||
min: 0.25,
|
||||
max: 3.0,
|
||||
divisions: 11,
|
||||
onChanged: (newValue) => settingsStore.emoteScale = newValue,
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Emote scale',
|
||||
trailing: '${settingsStore.emoteScale.toStringAsFixed(2)}x',
|
||||
value: settingsStore.emoteScale,
|
||||
min: 0.25,
|
||||
max: 3.0,
|
||||
divisions: 11,
|
||||
onChanged: (newValue) => settingsStore.emoteScale = newValue,
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Message scale'),
|
||||
const Spacer(),
|
||||
Text('${settingsStore.messageScale.toStringAsFixed(2)}x'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.messageScale,
|
||||
min: 0.5,
|
||||
max: 2.0,
|
||||
divisions: 6,
|
||||
onChanged: (newValue) => settingsStore.messageScale = newValue,
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Message scale',
|
||||
trailing: '${settingsStore.messageScale.toStringAsFixed(2)}x',
|
||||
value: settingsStore.messageScale,
|
||||
min: 0.5,
|
||||
max: 2.0,
|
||||
divisions: 6,
|
||||
onChanged: (newValue) => settingsStore.messageScale = newValue,
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Message spacing'),
|
||||
const Spacer(),
|
||||
Text(settingsStore.messageSpacing.toStringAsFixed(0).toString()),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.messageSpacing,
|
||||
min: 0.0,
|
||||
max: 30.0,
|
||||
divisions: 6,
|
||||
onChanged: (newValue) => settingsStore.messageSpacing = newValue,
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Message spacing',
|
||||
trailing: '${settingsStore.messageSpacing.toStringAsFixed(0)}px',
|
||||
value: settingsStore.messageSpacing,
|
||||
max: 30.0,
|
||||
divisions: 6,
|
||||
onChanged: (newValue) => settingsStore.messageSpacing = newValue,
|
||||
),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Font size'),
|
||||
const Spacer(),
|
||||
Text(settingsStore.fontSize.toInt().toString()),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.fontSize,
|
||||
min: 5,
|
||||
max: 20,
|
||||
divisions: 15,
|
||||
onChanged: (newValue) => settingsStore.fontSize = newValue,
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Font size',
|
||||
trailing: settingsStore.fontSize.toInt().toString(),
|
||||
value: settingsStore.fontSize,
|
||||
min: 5,
|
||||
max: 20,
|
||||
divisions: 15,
|
||||
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('Restores the original message of deleted messages.'),
|
||||
value: settingsStore.showDeletedMessages,
|
||||
onChanged: (newValue) => settingsStore.showDeletedMessages = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show message dividers',
|
||||
value: settingsStore.showChatMessageDividers,
|
||||
onChanged: (newValue) => settingsStore.showChatMessageDividers = newValue,
|
||||
),
|
||||
SettingsListSelect(
|
||||
title: 'Message timestamps',
|
||||
selectedOption: timestamps[settingsStore.timestampType.index],
|
||||
options: timestamps,
|
||||
onChanged: (newValue) => settingsStore.timestampType = TimestampType.values[timestamps.indexOf(newValue)],
|
||||
),
|
||||
const SectionHeader('Delay'),
|
||||
SettingsListSlider(
|
||||
title: 'Message delay',
|
||||
trailing: '${settingsStore.chatDelay.toInt()}s',
|
||||
subtitle:
|
||||
'Adds a delay (in seconds) before each message is rendered in chat. ${Platform.isIOS ? '15 seconds is recommended for iOS.' : ''}',
|
||||
value: settingsStore.chatDelay,
|
||||
max: 30.0,
|
||||
divisions: 30,
|
||||
onChanged: (newValue) => settingsStore.chatDelay = newValue,
|
||||
),
|
||||
const SectionHeader('Alerts'),
|
||||
SettingsListSwitch(
|
||||
title: 'Highlight first time chatters',
|
||||
value: settingsStore.highlightFirstTimeChatter,
|
||||
onChanged: (newValue) => settingsStore.highlightFirstTimeChatter = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Show notices',
|
||||
subtitle: const Text('Shows notices such as subs and re-subs, announcements, and raids.'),
|
||||
value: settingsStore.showUserNotices,
|
||||
onChanged: (newValue) => settingsStore.showUserNotices = newValue,
|
||||
),
|
||||
const SectionHeader('Layout'),
|
||||
SettingsListSwitch(
|
||||
title: 'Show bottom bar',
|
||||
value: settingsStore.showBottomBar,
|
||||
onChanged: (newValue) => settingsStore.showBottomBar = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Move emote menu button left',
|
||||
subtitle: const Text('Places the emote menu button on the left side to avoid accidental presses.'),
|
||||
value: settingsStore.emoteMenuButtonOnLeft,
|
||||
onChanged: (newValue) => settingsStore.emoteMenuButtonOnLeft = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Move notifications to bottom',
|
||||
value: settingsStore.chatNotificationsOnBottom,
|
||||
onChanged: (newValue) => settingsStore.chatNotificationsOnBottom = newValue,
|
||||
),
|
||||
const SectionHeader('Landscape mode'),
|
||||
SettingsListSwitch(
|
||||
title: 'Move chat left',
|
||||
value: settingsStore.landscapeChatLeftSide,
|
||||
onChanged: (newValue) => settingsStore.landscapeChatLeftSide = newValue,
|
||||
),
|
||||
SettingsListSwitch(
|
||||
title: 'Force vertical chat',
|
||||
subtitle: const Text('Intended for tablets and other larger displays.'),
|
||||
value: settingsStore.landscapeForceVerticalChat,
|
||||
onChanged: (newValue) => settingsStore.landscapeForceVerticalChat = newValue,
|
||||
),
|
||||
SettingsListSelect(
|
||||
title: 'Fill notch side',
|
||||
subtitle: 'Overrides and fills the available space in devices with a display notch.',
|
||||
selectedOption: landscapeCutouts[settingsStore.landscapeCutout.index],
|
||||
options: landscapeCutouts,
|
||||
onChanged: (newValue) =>
|
||||
settingsStore.landscapeCutout = LandscapeCutoutType.values[landscapeCutouts.indexOf(newValue)],
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Chat width',
|
||||
trailing: '${(settingsStore.chatWidth * 100).toStringAsFixed(0)}%',
|
||||
value: settingsStore.chatWidth,
|
||||
min: 0.2,
|
||||
max: 0.6,
|
||||
divisions: 8,
|
||||
onChanged: (newValue) => settingsStore.chatWidth = newValue,
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Chat overlay opacity',
|
||||
trailing: '${(settingsStore.fullScreenChatOverlayOpacity * 100).toStringAsFixed(0)}%',
|
||||
subtitle: 'Sets the opacity (transparency) of the overlay chat in fullscreen mode.',
|
||||
value: settingsStore.fullScreenChatOverlayOpacity,
|
||||
divisions: 10,
|
||||
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('Autocomplete'),
|
||||
SettingsListSwitch(
|
||||
title: 'Show autocomplete bar',
|
||||
subtitle: const Text('Shows a bar containing matching emotes and mentions while typing.'),
|
||||
value: settingsStore.autocomplete,
|
||||
onChanged: (newValue) => settingsStore.autocomplete = newValue,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_select.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class GeneralSettings extends StatelessWidget {
|
||||
final SettingsStore settingsStore;
|
||||
|
|
@ -12,50 +15,29 @@ class GeneralSettings extends StatelessWidget {
|
|||
const themes = ['System', 'Light', 'Dark', 'Black'];
|
||||
|
||||
return Observer(
|
||||
builder: (context) => ExpansionTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text(
|
||||
'General',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
builder: (context) => ListView(
|
||||
children: [
|
||||
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(),
|
||||
),
|
||||
const SectionHeader('Display'),
|
||||
SettingsListSelect(
|
||||
title: 'Theme',
|
||||
selectedOption: themes[settingsStore.themeType.index],
|
||||
options: themes,
|
||||
onChanged: (newTheme) => settingsStore.themeType = ThemeType.values[themes.indexOf(newTheme)],
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Large stream card'),
|
||||
const SectionHeader('Stream card'),
|
||||
SettingsListSwitch(
|
||||
title: 'Use large stream card',
|
||||
value: settingsStore.largeStreamCard,
|
||||
onChanged: (newValue) => settingsStore.largeStreamCard = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Stream card thumbnails'),
|
||||
SettingsListSwitch(
|
||||
title: 'Show thumbnail',
|
||||
value: settingsStore.showThumbnails,
|
||||
onChanged: (newValue) => settingsStore.showThumbnails = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Stream uptime on thumbnails'),
|
||||
subtitle: const Text('Shows the uptime of the stream in the HH:MM:SS format.'),
|
||||
value: settingsStore.showThumbnailUptime,
|
||||
onChanged: settingsStore.showThumbnails ? (newValue) => settingsStore.showThumbnailUptime = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Launch URLs in external browser'),
|
||||
subtitle: const Text('Opens links in the default external browser.'),
|
||||
const SectionHeader('Links'),
|
||||
SettingsListSwitch(
|
||||
title: 'Open links in external browser',
|
||||
value: settingsStore.launchUrlExternal,
|
||||
onChanged: (newValue) => settingsStore.launchUrlExternal = newValue,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
|
@ -30,7 +32,7 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: 'Reset All Settings',
|
||||
content: const Text('Are you sure you want to reset all settings?'),
|
||||
message: 'Are you sure you want to reset all settings?',
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
|
|
@ -50,9 +52,8 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
fill: true,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.red.shade700,
|
||||
color: Colors.grey,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
|
|
@ -62,20 +63,11 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpansionTile(
|
||||
childrenPadding: const EdgeInsets.only(bottom: 10.0),
|
||||
leading: const Icon(Icons.help),
|
||||
title: const Text(
|
||||
'Other',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info),
|
||||
title: const Text('About Frosty'),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.info_outline_rounded),
|
||||
title: 'About Frosty',
|
||||
onTap: () async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
|
|
@ -91,23 +83,44 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.launch),
|
||||
title: const Text('Changelog'),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.launch_rounded),
|
||||
title: 'FAQ',
|
||||
onTap: () => launchUrl(Uri.parse('https://www.frostyapp.io/#faq'),
|
||||
mode: widget.settingsStore.launchUrlExternal ? LaunchMode.externalApplication : LaunchMode.inAppWebView),
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.launch_rounded),
|
||||
title: 'Changelog',
|
||||
onTap: () => launchUrl(Uri.parse('https://github.com/tommyxchow/frosty/releases'),
|
||||
mode: widget.settingsStore.launchUrlExternal ? LaunchMode.externalApplication : LaunchMode.inAppWebView),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.launch),
|
||||
title: const Text('FAQ'),
|
||||
onTap: () => launchUrl(Uri.parse('https://github.com/tommyxchow/frosty#faq'),
|
||||
mode: widget.settingsStore.launchUrlExternal ? LaunchMode.externalApplication : LaunchMode.inAppWebView),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.delete_outline_rounded),
|
||||
title: 'Clear image cache',
|
||||
onTap: () async {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
await DefaultCacheManager().emptyCache();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: AlertMessage(message: 'Image cache cleared'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.restore_rounded),
|
||||
title: 'Reset settings',
|
||||
onTap: () => _showConfirmDialog(context),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => SwitchListTile.adaptive(
|
||||
title: const Text('Send anonymous crash logs'),
|
||||
isThreeLine: true,
|
||||
subtitle: const Text('Help improve Frosty by sending anonymous crash logs through Sentry.'),
|
||||
builder: (_) => SettingsListSwitch(
|
||||
title: 'Send anonymous crash logs',
|
||||
subtitle: const Text('Help improve Frosty by sending anonymous crash logs through Sentry.io.'),
|
||||
value: widget.settingsStore.sendCrashLogs,
|
||||
onChanged: (newValue) {
|
||||
if (newValue == true) {
|
||||
|
|
@ -119,41 +132,6 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
},
|
||||
),
|
||||
),
|
||||
...[
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
|
||||
icon: const Icon(Icons.delete_sweep),
|
||||
child: const Text('Clear image cache'),
|
||||
onPressed: () async {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
await DefaultCacheManager().emptyCache();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: AlertMessage(message: 'Image cache cleared'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
|
||||
onPressed: () => _showConfirmDialog(context),
|
||||
icon: const Icon(Icons.restore),
|
||||
child: const Text('Reset all settings'),
|
||||
)
|
||||
].map(
|
||||
(button) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15.0,
|
||||
vertical: 5.0,
|
||||
),
|
||||
width: double.infinity,
|
||||
child: button,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:frosty/screens/settings/account_settings.dart';
|
||||
import 'package:frosty/screens/settings/account/widgets/profile_card.dart';
|
||||
import 'package:frosty/screens/settings/chat_settings.dart';
|
||||
import 'package:frosty/screens/settings/general_settings.dart';
|
||||
import 'package:frosty/screens/settings/other_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/video_settings.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_tile_route.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simple_icons/simple_icons.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class Settings extends StatelessWidget {
|
||||
|
|
@ -20,21 +23,21 @@ class Settings extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
appBar: FrostyAppBar(
|
||||
title: const Text('Settings'),
|
||||
actions: [
|
||||
if (Platform.isAndroid)
|
||||
IconButton(
|
||||
tooltip: 'Support the app',
|
||||
tooltip: 'Support Frosty',
|
||||
onPressed: () => launchUrl(Uri.parse('https://www.buymeacoffee.com/tommychow'),
|
||||
mode: settingsStore.launchUrlExternal ? LaunchMode.externalApplication : LaunchMode.inAppWebView),
|
||||
icon: const FaIcon(FontAwesomeIcons.circleDollarToSlot),
|
||||
icon: const Icon(SimpleIcons.buymeacoffee),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'View source on GitHub',
|
||||
onPressed: () => launchUrl(Uri.parse('https://github.com/tommyxchow/frosty'),
|
||||
mode: settingsStore.launchUrlExternal ? LaunchMode.externalApplication : LaunchMode.inAppWebView),
|
||||
icon: const FaIcon(FontAwesomeIcons.github),
|
||||
icon: const Icon(SimpleIcons.github),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -42,15 +45,26 @@ class Settings extends StatelessWidget {
|
|||
bottom: false,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 10.0),
|
||||
AccountSettings(
|
||||
settingsStore: settingsStore,
|
||||
authStore: context.read<AuthStore>(),
|
||||
const SectionHeader('Account'),
|
||||
ProfileCard(authStore: context.read<AuthStore>()),
|
||||
const SectionHeader('Customize'),
|
||||
SettingsTileRoute(
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
title: 'General',
|
||||
child: GeneralSettings(settingsStore: settingsStore),
|
||||
),
|
||||
GeneralSettings(settingsStore: settingsStore),
|
||||
VideoSettings(settingsStore: settingsStore),
|
||||
ChatSettings(settingsStore: settingsStore),
|
||||
OtherSettings(settingsStore: settingsStore),
|
||||
SettingsTileRoute(
|
||||
leading: const Icon(Icons.tv_rounded),
|
||||
title: 'Video',
|
||||
child: VideoSettings(settingsStore: settingsStore),
|
||||
),
|
||||
SettingsTileRoute(
|
||||
leading: const Icon(Icons.chat_outlined),
|
||||
title: 'Chat',
|
||||
child: ChatSettings(settingsStore: settingsStore),
|
||||
),
|
||||
const SectionHeader('Other'),
|
||||
OtherSettings(settingsStore: settingsStore)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/constants.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/screens/settings/stores/user_store.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
|
@ -88,6 +90,52 @@ abstract class AuthBase with Store {
|
|||
return NavigationDecision.navigate;
|
||||
}
|
||||
|
||||
/// Shows a dialog verifying that the user is sure they want to block/unblock the target user.
|
||||
Future<void> showBlockDialog(
|
||||
BuildContext context, {
|
||||
required String targetUser,
|
||||
required String targetUserId,
|
||||
}) {
|
||||
final isBlocked = user.blockedUsers.where((blockedUser) => blockedUser.userId == targetUserId).isNotEmpty;
|
||||
|
||||
final title = isBlocked ? 'Unblock $targetUser' : 'Block $targetUser';
|
||||
|
||||
final message =
|
||||
'Are you sure you want to ${isBlocked ? 'unblock $targetUser?' : 'block $targetUser? This will remove them from channel lists, search results, and chat messages.'}';
|
||||
|
||||
void onPressed() {
|
||||
if (isBlocked) {
|
||||
user.unblock(targetId: targetUserId, headers: headersTwitch);
|
||||
} else {
|
||||
user.block(
|
||||
targetId: targetUserId,
|
||||
displayName: targetUser,
|
||||
headers: headersTwitch,
|
||||
);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: title,
|
||||
message: message,
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: onPressed,
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.grey,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AuthBase({required this.twitchApi}) : user = UserStore(twitchApi: twitchApi);
|
||||
|
||||
/// Initialize by retrieving a token if it does not already exist.
|
||||
|
|
|
|||
|
|
@ -13,16 +13,22 @@ class SettingsStore extends _SettingsStoreBase with _$SettingsStore {
|
|||
|
||||
abstract class _SettingsStoreBase with Store {
|
||||
// * General Settings
|
||||
// Theme defaults
|
||||
static const defaultThemeType = ThemeType.system;
|
||||
|
||||
// Stream card defaults
|
||||
static const defaultShowThumbnails = true;
|
||||
static const defaultLargeStreamCard = false;
|
||||
static const defaultShowThumbnailUptime = false;
|
||||
|
||||
// Links defaults
|
||||
static const defaultLaunchUrlExternal = false;
|
||||
|
||||
// Theme options
|
||||
@JsonKey(defaultValue: defaultThemeType, unknownEnumValue: ThemeType.system)
|
||||
@observable
|
||||
var themeType = defaultThemeType;
|
||||
|
||||
// Stream card options
|
||||
@JsonKey(defaultValue: defaultShowThumbnails)
|
||||
@observable
|
||||
var showThumbnails = defaultShowThumbnails;
|
||||
|
|
@ -31,10 +37,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var largeStreamCard = defaultLargeStreamCard;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowThumbnailUptime)
|
||||
@observable
|
||||
var showThumbnailUptime = defaultShowThumbnailUptime;
|
||||
|
||||
// Links options
|
||||
@JsonKey(defaultValue: defaultLaunchUrlExternal)
|
||||
@observable
|
||||
var launchUrlExternal = defaultLaunchUrlExternal;
|
||||
|
|
@ -42,22 +45,28 @@ abstract class _SettingsStoreBase with Store {
|
|||
@action
|
||||
void resetGeneralSettings() {
|
||||
themeType = defaultThemeType;
|
||||
showThumbnails = defaultShowThumbnails;
|
||||
|
||||
largeStreamCard = defaultLargeStreamCard;
|
||||
showThumbnailUptime = defaultShowThumbnailUptime;
|
||||
showThumbnails = defaultShowThumbnails;
|
||||
|
||||
launchUrlExternal = defaultLaunchUrlExternal;
|
||||
}
|
||||
|
||||
// * Video Settings
|
||||
// Player defaults
|
||||
static const defaultShowVideo = true;
|
||||
|
||||
// Overlay defaults
|
||||
static const defaultShowOverlay = true;
|
||||
static const defaultToggleableOverlay = false;
|
||||
static const defaultOverlayOpacity = 0.8;
|
||||
static const defaultOverlayOpacity = 0.5;
|
||||
|
||||
// Player options
|
||||
@JsonKey(defaultValue: defaultShowVideo)
|
||||
@observable
|
||||
var showVideo = defaultShowVideo;
|
||||
|
||||
// Overlay options
|
||||
@JsonKey(defaultValue: defaultShowOverlay)
|
||||
@observable
|
||||
var showOverlay = defaultShowOverlay;
|
||||
|
|
@ -73,107 +82,52 @@ abstract class _SettingsStoreBase with Store {
|
|||
@action
|
||||
void resetVideoSettings() {
|
||||
showVideo = defaultShowVideo;
|
||||
|
||||
showOverlay = defaultShowOverlay;
|
||||
toggleableOverlay = defaultToggleableOverlay;
|
||||
overlayOpacity = defaultOverlayOpacity;
|
||||
}
|
||||
|
||||
// * Chat Settings
|
||||
static const defaultChatDelay = 0.0;
|
||||
static const defaultChatOnlyPreventSleep = true;
|
||||
static const defaultAutocomplete = true;
|
||||
static const defaultLandscapeCutout = LandscapeCutoutType.none;
|
||||
|
||||
static const defaultShowBottomBar = true;
|
||||
static const defaultEmoteMenuButtonOnLeft = false;
|
||||
static const defaultLandscapeChatLeftSide = false;
|
||||
static const defaultLandscapeForceVerticalChat = false;
|
||||
static const defaultChatNotificationsOnBottom = false;
|
||||
static const defaultChatWidth = 0.3;
|
||||
static const defaultFullScreenChatOverlayOpacity = 0.5;
|
||||
|
||||
static const defaultUseReadableColors = true;
|
||||
static const defaultShowDeletedMessages = false;
|
||||
static const defaultShowChatMessageDividers = false;
|
||||
static const defaultTimestampType = TimestampType.disabled;
|
||||
|
||||
static const defaultHighlightFirstTimeChatter = true;
|
||||
static const defaultShowUserNotices = true;
|
||||
|
||||
// Message sizing defaults
|
||||
static const defaultBadgeScale = 1.0;
|
||||
static const defaultEmoteScale = 1.0;
|
||||
static const defaultMessageScale = 1.0;
|
||||
static const defaultMessageSpacing = 10.0;
|
||||
static const defaultFontSize = 12.0;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatDelay)
|
||||
@observable
|
||||
var chatDelay = defaultChatDelay;
|
||||
// Message appearance defaults
|
||||
static const defaultUseReadableColors = true;
|
||||
static const defaultShowDeletedMessages = false;
|
||||
static const defaultShowChatMessageDividers = false;
|
||||
static const defaultTimestampType = TimestampType.disabled;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatOnlyPreventSleep)
|
||||
@observable
|
||||
var chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
|
||||
// Delay defaults
|
||||
static const defaultChatDelay = 0.0;
|
||||
|
||||
@JsonKey(defaultValue: defaultAutocomplete)
|
||||
@observable
|
||||
var autocomplete = defaultAutocomplete;
|
||||
// Alert defaults
|
||||
static const defaultHighlightFirstTimeChatter = true;
|
||||
static const defaultShowUserNotices = true;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowBottomBar)
|
||||
@observable
|
||||
var showBottomBar = defaultShowBottomBar;
|
||||
// Layout defaults
|
||||
static const defaultShowBottomBar = true;
|
||||
static const defaultEmoteMenuButtonOnLeft = false;
|
||||
static const defaultChatNotificationsOnBottom = false;
|
||||
|
||||
@JsonKey(defaultValue: defaultEmoteMenuButtonOnLeft)
|
||||
@observable
|
||||
var emoteMenuButtonOnLeft = defaultEmoteMenuButtonOnLeft;
|
||||
// Landscape mode defaults
|
||||
static const defaultLandscapeChatLeftSide = false;
|
||||
static const defaultLandscapeForceVerticalChat = false;
|
||||
static const defaultLandscapeCutout = LandscapeCutoutType.none;
|
||||
static const defaultChatWidth = 0.3;
|
||||
static const defaultFullScreenChatOverlayOpacity = 0.5;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeChatLeftSide)
|
||||
@observable
|
||||
var landscapeChatLeftSide = defaultLandscapeChatLeftSide;
|
||||
// Sleep defaults
|
||||
static const defaultChatOnlyPreventSleep = false;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeForceVerticalChat)
|
||||
@observable
|
||||
var landscapeForceVerticalChat = defaultLandscapeForceVerticalChat;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatNotificationsOnBottom)
|
||||
@observable
|
||||
var chatNotificationsOnBottom = defaultChatNotificationsOnBottom;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeCutout)
|
||||
@observable
|
||||
var landscapeCutout = defaultLandscapeCutout;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatWidth)
|
||||
@observable
|
||||
var chatWidth = defaultChatWidth;
|
||||
|
||||
@JsonKey(defaultValue: defaultFullScreenChatOverlayOpacity)
|
||||
@observable
|
||||
var fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
|
||||
|
||||
@JsonKey(defaultValue: defaultUseReadableColors)
|
||||
@observable
|
||||
var useReadableColors = defaultUseReadableColors;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowDeletedMessages)
|
||||
@observable
|
||||
var showDeletedMessages = defaultShowDeletedMessages;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowChatMessageDividers)
|
||||
@observable
|
||||
var showChatMessageDividers = defaultShowChatMessageDividers;
|
||||
|
||||
@JsonKey(defaultValue: defaultTimestampType, unknownEnumValue: TimestampType.disabled)
|
||||
@observable
|
||||
var timestampType = defaultTimestampType;
|
||||
|
||||
@JsonKey(defaultValue: defaultHighlightFirstTimeChatter)
|
||||
@observable
|
||||
var highlightFirstTimeChatter = defaultHighlightFirstTimeChatter;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowUserNotices)
|
||||
@observable
|
||||
var showUserNotices = defaultHighlightFirstTimeChatter;
|
||||
// Autocomplete defaults
|
||||
static const defaultAutocomplete = true;
|
||||
|
||||
// Message sizing options
|
||||
@JsonKey(defaultValue: defaultBadgeScale)
|
||||
@observable
|
||||
var badgeScale = defaultBadgeScale;
|
||||
|
|
@ -194,30 +148,111 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var fontSize = defaultFontSize;
|
||||
|
||||
// Message appearance options
|
||||
@JsonKey(defaultValue: defaultUseReadableColors)
|
||||
@observable
|
||||
var useReadableColors = defaultUseReadableColors;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowDeletedMessages)
|
||||
@observable
|
||||
var showDeletedMessages = defaultShowDeletedMessages;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowChatMessageDividers)
|
||||
@observable
|
||||
var showChatMessageDividers = defaultShowChatMessageDividers;
|
||||
|
||||
@JsonKey(defaultValue: defaultTimestampType, unknownEnumValue: TimestampType.disabled)
|
||||
@observable
|
||||
var timestampType = defaultTimestampType;
|
||||
|
||||
// Delay options
|
||||
@JsonKey(defaultValue: defaultChatDelay)
|
||||
@observable
|
||||
var chatDelay = defaultChatDelay;
|
||||
|
||||
// Alert options
|
||||
@JsonKey(defaultValue: defaultHighlightFirstTimeChatter)
|
||||
@observable
|
||||
var highlightFirstTimeChatter = defaultHighlightFirstTimeChatter;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowUserNotices)
|
||||
@observable
|
||||
var showUserNotices = defaultHighlightFirstTimeChatter;
|
||||
|
||||
// Layout options
|
||||
@JsonKey(defaultValue: defaultShowBottomBar)
|
||||
@observable
|
||||
var showBottomBar = defaultShowBottomBar;
|
||||
|
||||
@JsonKey(defaultValue: defaultEmoteMenuButtonOnLeft)
|
||||
@observable
|
||||
var emoteMenuButtonOnLeft = defaultEmoteMenuButtonOnLeft;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatNotificationsOnBottom)
|
||||
@observable
|
||||
var chatNotificationsOnBottom = defaultChatNotificationsOnBottom;
|
||||
|
||||
// Landscape mode options
|
||||
@JsonKey(defaultValue: defaultLandscapeChatLeftSide)
|
||||
@observable
|
||||
var landscapeChatLeftSide = defaultLandscapeChatLeftSide;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeForceVerticalChat)
|
||||
@observable
|
||||
var landscapeForceVerticalChat = defaultLandscapeForceVerticalChat;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeCutout)
|
||||
@observable
|
||||
var landscapeCutout = defaultLandscapeCutout;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatWidth)
|
||||
@observable
|
||||
var chatWidth = defaultChatWidth;
|
||||
|
||||
@JsonKey(defaultValue: defaultFullScreenChatOverlayOpacity)
|
||||
@observable
|
||||
var fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
|
||||
|
||||
// Sleep options
|
||||
@JsonKey(defaultValue: defaultChatOnlyPreventSleep)
|
||||
@observable
|
||||
var chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
|
||||
|
||||
// Autocomplete options
|
||||
@JsonKey(defaultValue: defaultAutocomplete)
|
||||
@observable
|
||||
var autocomplete = defaultAutocomplete;
|
||||
|
||||
@action
|
||||
void resetChatSettings() {
|
||||
chatDelay = defaultChatDelay;
|
||||
chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
|
||||
autocomplete = defaultAutocomplete;
|
||||
showBottomBar = defaultShowBottomBar;
|
||||
emoteMenuButtonOnLeft = defaultEmoteMenuButtonOnLeft;
|
||||
landscapeChatLeftSide = defaultLandscapeChatLeftSide;
|
||||
landscapeForceVerticalChat = defaultLandscapeForceVerticalChat;
|
||||
chatNotificationsOnBottom = defaultChatNotificationsOnBottom;
|
||||
landscapeCutout = defaultLandscapeCutout;
|
||||
chatWidth = defaultChatWidth;
|
||||
fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
|
||||
useReadableColors = defaultUseReadableColors;
|
||||
showDeletedMessages = defaultShowDeletedMessages;
|
||||
showChatMessageDividers = defaultShowChatMessageDividers;
|
||||
timestampType = defaultTimestampType;
|
||||
highlightFirstTimeChatter = defaultHighlightFirstTimeChatter;
|
||||
showUserNotices = defaultShowUserNotices;
|
||||
badgeScale = defaultBadgeScale;
|
||||
emoteScale = defaultEmoteScale;
|
||||
messageScale = defaultMessageScale;
|
||||
messageSpacing = defaultMessageSpacing;
|
||||
fontSize = defaultFontSize;
|
||||
|
||||
useReadableColors = defaultUseReadableColors;
|
||||
showDeletedMessages = defaultShowDeletedMessages;
|
||||
showChatMessageDividers = defaultShowChatMessageDividers;
|
||||
timestampType = defaultTimestampType;
|
||||
|
||||
chatDelay = defaultChatDelay;
|
||||
|
||||
highlightFirstTimeChatter = defaultHighlightFirstTimeChatter;
|
||||
showUserNotices = defaultShowUserNotices;
|
||||
|
||||
showBottomBar = defaultShowBottomBar;
|
||||
emoteMenuButtonOnLeft = defaultEmoteMenuButtonOnLeft;
|
||||
chatNotificationsOnBottom = defaultChatNotificationsOnBottom;
|
||||
|
||||
landscapeChatLeftSide = defaultLandscapeChatLeftSide;
|
||||
landscapeForceVerticalChat = defaultLandscapeForceVerticalChat;
|
||||
landscapeCutout = defaultLandscapeCutout;
|
||||
chatWidth = defaultChatWidth;
|
||||
fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
|
||||
|
||||
chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
|
||||
autocomplete = defaultAutocomplete;
|
||||
}
|
||||
|
||||
// * Other settings
|
||||
|
|
@ -234,17 +269,12 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
// * Global configs
|
||||
static const defaultFullScreen = false;
|
||||
static const defaultExpandInfo = true;
|
||||
static const defaultFullScreenChatOverlay = false;
|
||||
|
||||
@JsonKey(defaultValue: defaultFullScreen)
|
||||
@observable
|
||||
var fullScreen = defaultFullScreen;
|
||||
|
||||
@JsonKey(defaultValue: defaultExpandInfo)
|
||||
@observable
|
||||
var expandInfo = defaultExpandInfo;
|
||||
|
||||
@JsonKey(defaultValue: defaultFullScreenChatOverlay)
|
||||
@observable
|
||||
var fullScreenChatOverlay = defaultFullScreenChatOverlay;
|
||||
|
|
@ -252,7 +282,6 @@ abstract class _SettingsStoreBase with Store {
|
|||
@action
|
||||
void resetGlobalConfigs() {
|
||||
fullScreen = defaultFullScreen;
|
||||
expandInfo = defaultExpandInfo;
|
||||
fullScreenChatOverlay = defaultFullScreenChatOverlay;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,28 +13,16 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
ThemeType.system
|
||||
..showThumbnails = json['showThumbnails'] as bool? ?? true
|
||||
..largeStreamCard = json['largeStreamCard'] as bool? ?? false
|
||||
..showThumbnailUptime = json['showThumbnailUptime'] as bool? ?? false
|
||||
..launchUrlExternal = json['launchUrlExternal'] as bool? ?? false
|
||||
..showVideo = json['showVideo'] as bool? ?? true
|
||||
..showOverlay = json['showOverlay'] as bool? ?? true
|
||||
..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false
|
||||
..overlayOpacity = (json['overlayOpacity'] as num?)?.toDouble() ?? 0.8
|
||||
..chatDelay = (json['chatDelay'] as num?)?.toDouble() ?? 0.0
|
||||
..chatOnlyPreventSleep = json['chatOnlyPreventSleep'] as bool? ?? true
|
||||
..autocomplete = json['autocomplete'] as bool? ?? true
|
||||
..showBottomBar = json['showBottomBar'] as bool? ?? true
|
||||
..emoteMenuButtonOnLeft = json['emoteMenuButtonOnLeft'] as bool? ?? false
|
||||
..landscapeChatLeftSide = json['landscapeChatLeftSide'] as bool? ?? false
|
||||
..landscapeForceVerticalChat =
|
||||
json['landscapeForceVerticalChat'] as bool? ?? false
|
||||
..chatNotificationsOnBottom =
|
||||
json['chatNotificationsOnBottom'] as bool? ?? false
|
||||
..landscapeCutout = $enumDecodeNullable(
|
||||
_$LandscapeCutoutTypeEnumMap, json['landscapeCutout']) ??
|
||||
LandscapeCutoutType.none
|
||||
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.3
|
||||
..fullScreenChatOverlayOpacity =
|
||||
(json['fullScreenChatOverlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
..overlayOpacity = (json['overlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
..badgeScale = (json['badgeScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteScale = (json['emoteScale'] as num?)?.toDouble() ?? 1.0
|
||||
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
|
||||
..messageSpacing = (json['messageSpacing'] as num?)?.toDouble() ?? 10.0
|
||||
..fontSize = (json['fontSize'] as num?)?.toDouble() ?? 12.0
|
||||
..useReadableColors = json['useReadableColors'] as bool? ?? true
|
||||
..showDeletedMessages = json['showDeletedMessages'] as bool? ?? false
|
||||
..showChatMessageDividers =
|
||||
|
|
@ -43,17 +31,27 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
_$TimestampTypeEnumMap, json['timestampType'],
|
||||
unknownValue: TimestampType.disabled) ??
|
||||
TimestampType.disabled
|
||||
..chatDelay = (json['chatDelay'] as num?)?.toDouble() ?? 0.0
|
||||
..highlightFirstTimeChatter =
|
||||
json['highlightFirstTimeChatter'] as bool? ?? true
|
||||
..showUserNotices = json['showUserNotices'] as bool? ?? true
|
||||
..badgeScale = (json['badgeScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteScale = (json['emoteScale'] as num?)?.toDouble() ?? 1.0
|
||||
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
|
||||
..messageSpacing = (json['messageSpacing'] as num?)?.toDouble() ?? 10.0
|
||||
..fontSize = (json['fontSize'] as num?)?.toDouble() ?? 12.0
|
||||
..showBottomBar = json['showBottomBar'] as bool? ?? true
|
||||
..emoteMenuButtonOnLeft = json['emoteMenuButtonOnLeft'] as bool? ?? false
|
||||
..chatNotificationsOnBottom =
|
||||
json['chatNotificationsOnBottom'] as bool? ?? false
|
||||
..landscapeChatLeftSide = json['landscapeChatLeftSide'] as bool? ?? false
|
||||
..landscapeForceVerticalChat =
|
||||
json['landscapeForceVerticalChat'] as bool? ?? false
|
||||
..landscapeCutout = $enumDecodeNullable(
|
||||
_$LandscapeCutoutTypeEnumMap, json['landscapeCutout']) ??
|
||||
LandscapeCutoutType.none
|
||||
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.3
|
||||
..fullScreenChatOverlayOpacity =
|
||||
(json['fullScreenChatOverlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
..chatOnlyPreventSleep = json['chatOnlyPreventSleep'] as bool? ?? false
|
||||
..autocomplete = json['autocomplete'] as bool? ?? true
|
||||
..sendCrashLogs = json['sendCrashLogs'] as bool? ?? true
|
||||
..fullScreen = json['fullScreen'] as bool? ?? false
|
||||
..expandInfo = json['expandInfo'] as bool? ?? true
|
||||
..fullScreenChatOverlay = json['fullScreenChatOverlay'] as bool? ?? false;
|
||||
|
||||
Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
||||
|
|
@ -61,38 +59,36 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'themeType': _$ThemeTypeEnumMap[instance.themeType]!,
|
||||
'showThumbnails': instance.showThumbnails,
|
||||
'largeStreamCard': instance.largeStreamCard,
|
||||
'showThumbnailUptime': instance.showThumbnailUptime,
|
||||
'launchUrlExternal': instance.launchUrlExternal,
|
||||
'showVideo': instance.showVideo,
|
||||
'showOverlay': instance.showOverlay,
|
||||
'toggleableOverlay': instance.toggleableOverlay,
|
||||
'overlayOpacity': instance.overlayOpacity,
|
||||
'chatDelay': instance.chatDelay,
|
||||
'chatOnlyPreventSleep': instance.chatOnlyPreventSleep,
|
||||
'autocomplete': instance.autocomplete,
|
||||
'showBottomBar': instance.showBottomBar,
|
||||
'emoteMenuButtonOnLeft': instance.emoteMenuButtonOnLeft,
|
||||
'landscapeChatLeftSide': instance.landscapeChatLeftSide,
|
||||
'landscapeForceVerticalChat': instance.landscapeForceVerticalChat,
|
||||
'chatNotificationsOnBottom': instance.chatNotificationsOnBottom,
|
||||
'landscapeCutout':
|
||||
_$LandscapeCutoutTypeEnumMap[instance.landscapeCutout]!,
|
||||
'chatWidth': instance.chatWidth,
|
||||
'fullScreenChatOverlayOpacity': instance.fullScreenChatOverlayOpacity,
|
||||
'useReadableColors': instance.useReadableColors,
|
||||
'showDeletedMessages': instance.showDeletedMessages,
|
||||
'showChatMessageDividers': instance.showChatMessageDividers,
|
||||
'timestampType': _$TimestampTypeEnumMap[instance.timestampType]!,
|
||||
'highlightFirstTimeChatter': instance.highlightFirstTimeChatter,
|
||||
'showUserNotices': instance.showUserNotices,
|
||||
'badgeScale': instance.badgeScale,
|
||||
'emoteScale': instance.emoteScale,
|
||||
'messageScale': instance.messageScale,
|
||||
'messageSpacing': instance.messageSpacing,
|
||||
'fontSize': instance.fontSize,
|
||||
'useReadableColors': instance.useReadableColors,
|
||||
'showDeletedMessages': instance.showDeletedMessages,
|
||||
'showChatMessageDividers': instance.showChatMessageDividers,
|
||||
'timestampType': _$TimestampTypeEnumMap[instance.timestampType]!,
|
||||
'chatDelay': instance.chatDelay,
|
||||
'highlightFirstTimeChatter': instance.highlightFirstTimeChatter,
|
||||
'showUserNotices': instance.showUserNotices,
|
||||
'showBottomBar': instance.showBottomBar,
|
||||
'emoteMenuButtonOnLeft': instance.emoteMenuButtonOnLeft,
|
||||
'chatNotificationsOnBottom': instance.chatNotificationsOnBottom,
|
||||
'landscapeChatLeftSide': instance.landscapeChatLeftSide,
|
||||
'landscapeForceVerticalChat': instance.landscapeForceVerticalChat,
|
||||
'landscapeCutout':
|
||||
_$LandscapeCutoutTypeEnumMap[instance.landscapeCutout]!,
|
||||
'chatWidth': instance.chatWidth,
|
||||
'fullScreenChatOverlayOpacity': instance.fullScreenChatOverlayOpacity,
|
||||
'chatOnlyPreventSleep': instance.chatOnlyPreventSleep,
|
||||
'autocomplete': instance.autocomplete,
|
||||
'sendCrashLogs': instance.sendCrashLogs,
|
||||
'fullScreen': instance.fullScreen,
|
||||
'expandInfo': instance.expandInfo,
|
||||
'fullScreenChatOverlay': instance.fullScreenChatOverlay,
|
||||
};
|
||||
|
||||
|
|
@ -103,6 +99,12 @@ const _$ThemeTypeEnumMap = {
|
|||
ThemeType.black: 'black',
|
||||
};
|
||||
|
||||
const _$TimestampTypeEnumMap = {
|
||||
TimestampType.disabled: 'disabled',
|
||||
TimestampType.twelve: 'twelve',
|
||||
TimestampType.twentyFour: 'twentyFour',
|
||||
};
|
||||
|
||||
const _$LandscapeCutoutTypeEnumMap = {
|
||||
LandscapeCutoutType.none: 'none',
|
||||
LandscapeCutoutType.left: 'left',
|
||||
|
|
@ -110,12 +112,6 @@ const _$LandscapeCutoutTypeEnumMap = {
|
|||
LandscapeCutoutType.both: 'both',
|
||||
};
|
||||
|
||||
const _$TimestampTypeEnumMap = {
|
||||
TimestampType.disabled: 'disabled',
|
||||
TimestampType.twelve: 'twelve',
|
||||
TimestampType.twentyFour: 'twentyFour',
|
||||
};
|
||||
|
||||
// **************************************************************************
|
||||
// StoreGenerator
|
||||
// **************************************************************************
|
||||
|
|
@ -171,22 +167,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$showThumbnailUptimeAtom =
|
||||
Atom(name: '_SettingsStoreBase.showThumbnailUptime', context: context);
|
||||
|
||||
@override
|
||||
bool get showThumbnailUptime {
|
||||
_$showThumbnailUptimeAtom.reportRead();
|
||||
return super.showThumbnailUptime;
|
||||
}
|
||||
|
||||
@override
|
||||
set showThumbnailUptime(bool value) {
|
||||
_$showThumbnailUptimeAtom.reportWrite(value, super.showThumbnailUptime, () {
|
||||
super.showThumbnailUptime = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$launchUrlExternalAtom =
|
||||
Atom(name: '_SettingsStoreBase.launchUrlExternal', context: context);
|
||||
|
||||
|
|
@ -267,287 +247,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$chatDelayAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatDelay', context: context);
|
||||
|
||||
@override
|
||||
double get chatDelay {
|
||||
_$chatDelayAtom.reportRead();
|
||||
return super.chatDelay;
|
||||
}
|
||||
|
||||
@override
|
||||
set chatDelay(double value) {
|
||||
_$chatDelayAtom.reportWrite(value, super.chatDelay, () {
|
||||
super.chatDelay = value;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@override
|
||||
bool get autocomplete {
|
||||
_$autocompleteAtom.reportRead();
|
||||
return super.autocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
set autocomplete(bool value) {
|
||||
_$autocompleteAtom.reportWrite(value, super.autocomplete, () {
|
||||
super.autocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showBottomBarAtom =
|
||||
Atom(name: '_SettingsStoreBase.showBottomBar', context: context);
|
||||
|
||||
@override
|
||||
bool get showBottomBar {
|
||||
_$showBottomBarAtom.reportRead();
|
||||
return super.showBottomBar;
|
||||
}
|
||||
|
||||
@override
|
||||
set showBottomBar(bool value) {
|
||||
_$showBottomBarAtom.reportWrite(value, super.showBottomBar, () {
|
||||
super.showBottomBar = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$emoteMenuButtonOnLeftAtom =
|
||||
Atom(name: '_SettingsStoreBase.emoteMenuButtonOnLeft', context: context);
|
||||
|
||||
@override
|
||||
bool get emoteMenuButtonOnLeft {
|
||||
_$emoteMenuButtonOnLeftAtom.reportRead();
|
||||
return super.emoteMenuButtonOnLeft;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteMenuButtonOnLeft(bool value) {
|
||||
_$emoteMenuButtonOnLeftAtom.reportWrite(value, super.emoteMenuButtonOnLeft,
|
||||
() {
|
||||
super.emoteMenuButtonOnLeft = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$landscapeChatLeftSideAtom =
|
||||
Atom(name: '_SettingsStoreBase.landscapeChatLeftSide', context: context);
|
||||
|
||||
@override
|
||||
bool get landscapeChatLeftSide {
|
||||
_$landscapeChatLeftSideAtom.reportRead();
|
||||
return super.landscapeChatLeftSide;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeChatLeftSide(bool value) {
|
||||
_$landscapeChatLeftSideAtom.reportWrite(value, super.landscapeChatLeftSide,
|
||||
() {
|
||||
super.landscapeChatLeftSide = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$landscapeForceVerticalChatAtom = Atom(
|
||||
name: '_SettingsStoreBase.landscapeForceVerticalChat', context: context);
|
||||
|
||||
@override
|
||||
bool get landscapeForceVerticalChat {
|
||||
_$landscapeForceVerticalChatAtom.reportRead();
|
||||
return super.landscapeForceVerticalChat;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeForceVerticalChat(bool value) {
|
||||
_$landscapeForceVerticalChatAtom
|
||||
.reportWrite(value, super.landscapeForceVerticalChat, () {
|
||||
super.landscapeForceVerticalChat = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatNotificationsOnBottomAtom = Atom(
|
||||
name: '_SettingsStoreBase.chatNotificationsOnBottom', context: context);
|
||||
|
||||
@override
|
||||
bool get chatNotificationsOnBottom {
|
||||
_$chatNotificationsOnBottomAtom.reportRead();
|
||||
return super.chatNotificationsOnBottom;
|
||||
}
|
||||
|
||||
@override
|
||||
set chatNotificationsOnBottom(bool value) {
|
||||
_$chatNotificationsOnBottomAtom
|
||||
.reportWrite(value, super.chatNotificationsOnBottom, () {
|
||||
super.chatNotificationsOnBottom = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$landscapeCutoutAtom =
|
||||
Atom(name: '_SettingsStoreBase.landscapeCutout', context: context);
|
||||
|
||||
@override
|
||||
LandscapeCutoutType get landscapeCutout {
|
||||
_$landscapeCutoutAtom.reportRead();
|
||||
return super.landscapeCutout;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeCutout(LandscapeCutoutType value) {
|
||||
_$landscapeCutoutAtom.reportWrite(value, super.landscapeCutout, () {
|
||||
super.landscapeCutout = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatWidthAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatWidth', context: context);
|
||||
|
||||
@override
|
||||
double get chatWidth {
|
||||
_$chatWidthAtom.reportRead();
|
||||
return super.chatWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
set chatWidth(double value) {
|
||||
_$chatWidthAtom.reportWrite(value, super.chatWidth, () {
|
||||
super.chatWidth = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$fullScreenChatOverlayOpacityAtom = Atom(
|
||||
name: '_SettingsStoreBase.fullScreenChatOverlayOpacity',
|
||||
context: context);
|
||||
|
||||
@override
|
||||
double get fullScreenChatOverlayOpacity {
|
||||
_$fullScreenChatOverlayOpacityAtom.reportRead();
|
||||
return super.fullScreenChatOverlayOpacity;
|
||||
}
|
||||
|
||||
@override
|
||||
set fullScreenChatOverlayOpacity(double value) {
|
||||
_$fullScreenChatOverlayOpacityAtom
|
||||
.reportWrite(value, super.fullScreenChatOverlayOpacity, () {
|
||||
super.fullScreenChatOverlayOpacity = value;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@override
|
||||
bool get showDeletedMessages {
|
||||
_$showDeletedMessagesAtom.reportRead();
|
||||
return super.showDeletedMessages;
|
||||
}
|
||||
|
||||
@override
|
||||
set showDeletedMessages(bool value) {
|
||||
_$showDeletedMessagesAtom.reportWrite(value, super.showDeletedMessages, () {
|
||||
super.showDeletedMessages = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showChatMessageDividersAtom = Atom(
|
||||
name: '_SettingsStoreBase.showChatMessageDividers', context: context);
|
||||
|
||||
@override
|
||||
bool get showChatMessageDividers {
|
||||
_$showChatMessageDividersAtom.reportRead();
|
||||
return super.showChatMessageDividers;
|
||||
}
|
||||
|
||||
@override
|
||||
set showChatMessageDividers(bool value) {
|
||||
_$showChatMessageDividersAtom
|
||||
.reportWrite(value, super.showChatMessageDividers, () {
|
||||
super.showChatMessageDividers = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$timestampTypeAtom =
|
||||
Atom(name: '_SettingsStoreBase.timestampType', context: context);
|
||||
|
||||
@override
|
||||
TimestampType get timestampType {
|
||||
_$timestampTypeAtom.reportRead();
|
||||
return super.timestampType;
|
||||
}
|
||||
|
||||
@override
|
||||
set timestampType(TimestampType value) {
|
||||
_$timestampTypeAtom.reportWrite(value, super.timestampType, () {
|
||||
super.timestampType = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$highlightFirstTimeChatterAtom = Atom(
|
||||
name: '_SettingsStoreBase.highlightFirstTimeChatter', context: context);
|
||||
|
||||
@override
|
||||
bool get highlightFirstTimeChatter {
|
||||
_$highlightFirstTimeChatterAtom.reportRead();
|
||||
return super.highlightFirstTimeChatter;
|
||||
}
|
||||
|
||||
@override
|
||||
set highlightFirstTimeChatter(bool value) {
|
||||
_$highlightFirstTimeChatterAtom
|
||||
.reportWrite(value, super.highlightFirstTimeChatter, () {
|
||||
super.highlightFirstTimeChatter = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showUserNoticesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showUserNotices', context: context);
|
||||
|
||||
@override
|
||||
bool get showUserNotices {
|
||||
_$showUserNoticesAtom.reportRead();
|
||||
return super.showUserNotices;
|
||||
}
|
||||
|
||||
@override
|
||||
set showUserNotices(bool value) {
|
||||
_$showUserNoticesAtom.reportWrite(value, super.showUserNotices, () {
|
||||
super.showUserNotices = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$badgeScaleAtom =
|
||||
Atom(name: '_SettingsStoreBase.badgeScale', context: context);
|
||||
|
||||
|
|
@ -628,6 +327,287 @@ 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);
|
||||
|
||||
@override
|
||||
bool get showDeletedMessages {
|
||||
_$showDeletedMessagesAtom.reportRead();
|
||||
return super.showDeletedMessages;
|
||||
}
|
||||
|
||||
@override
|
||||
set showDeletedMessages(bool value) {
|
||||
_$showDeletedMessagesAtom.reportWrite(value, super.showDeletedMessages, () {
|
||||
super.showDeletedMessages = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showChatMessageDividersAtom = Atom(
|
||||
name: '_SettingsStoreBase.showChatMessageDividers', context: context);
|
||||
|
||||
@override
|
||||
bool get showChatMessageDividers {
|
||||
_$showChatMessageDividersAtom.reportRead();
|
||||
return super.showChatMessageDividers;
|
||||
}
|
||||
|
||||
@override
|
||||
set showChatMessageDividers(bool value) {
|
||||
_$showChatMessageDividersAtom
|
||||
.reportWrite(value, super.showChatMessageDividers, () {
|
||||
super.showChatMessageDividers = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$timestampTypeAtom =
|
||||
Atom(name: '_SettingsStoreBase.timestampType', context: context);
|
||||
|
||||
@override
|
||||
TimestampType get timestampType {
|
||||
_$timestampTypeAtom.reportRead();
|
||||
return super.timestampType;
|
||||
}
|
||||
|
||||
@override
|
||||
set timestampType(TimestampType value) {
|
||||
_$timestampTypeAtom.reportWrite(value, super.timestampType, () {
|
||||
super.timestampType = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatDelayAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatDelay', context: context);
|
||||
|
||||
@override
|
||||
double get chatDelay {
|
||||
_$chatDelayAtom.reportRead();
|
||||
return super.chatDelay;
|
||||
}
|
||||
|
||||
@override
|
||||
set chatDelay(double value) {
|
||||
_$chatDelayAtom.reportWrite(value, super.chatDelay, () {
|
||||
super.chatDelay = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$highlightFirstTimeChatterAtom = Atom(
|
||||
name: '_SettingsStoreBase.highlightFirstTimeChatter', context: context);
|
||||
|
||||
@override
|
||||
bool get highlightFirstTimeChatter {
|
||||
_$highlightFirstTimeChatterAtom.reportRead();
|
||||
return super.highlightFirstTimeChatter;
|
||||
}
|
||||
|
||||
@override
|
||||
set highlightFirstTimeChatter(bool value) {
|
||||
_$highlightFirstTimeChatterAtom
|
||||
.reportWrite(value, super.highlightFirstTimeChatter, () {
|
||||
super.highlightFirstTimeChatter = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showUserNoticesAtom =
|
||||
Atom(name: '_SettingsStoreBase.showUserNotices', context: context);
|
||||
|
||||
@override
|
||||
bool get showUserNotices {
|
||||
_$showUserNoticesAtom.reportRead();
|
||||
return super.showUserNotices;
|
||||
}
|
||||
|
||||
@override
|
||||
set showUserNotices(bool value) {
|
||||
_$showUserNoticesAtom.reportWrite(value, super.showUserNotices, () {
|
||||
super.showUserNotices = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showBottomBarAtom =
|
||||
Atom(name: '_SettingsStoreBase.showBottomBar', context: context);
|
||||
|
||||
@override
|
||||
bool get showBottomBar {
|
||||
_$showBottomBarAtom.reportRead();
|
||||
return super.showBottomBar;
|
||||
}
|
||||
|
||||
@override
|
||||
set showBottomBar(bool value) {
|
||||
_$showBottomBarAtom.reportWrite(value, super.showBottomBar, () {
|
||||
super.showBottomBar = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$emoteMenuButtonOnLeftAtom =
|
||||
Atom(name: '_SettingsStoreBase.emoteMenuButtonOnLeft', context: context);
|
||||
|
||||
@override
|
||||
bool get emoteMenuButtonOnLeft {
|
||||
_$emoteMenuButtonOnLeftAtom.reportRead();
|
||||
return super.emoteMenuButtonOnLeft;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteMenuButtonOnLeft(bool value) {
|
||||
_$emoteMenuButtonOnLeftAtom.reportWrite(value, super.emoteMenuButtonOnLeft,
|
||||
() {
|
||||
super.emoteMenuButtonOnLeft = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatNotificationsOnBottomAtom = Atom(
|
||||
name: '_SettingsStoreBase.chatNotificationsOnBottom', context: context);
|
||||
|
||||
@override
|
||||
bool get chatNotificationsOnBottom {
|
||||
_$chatNotificationsOnBottomAtom.reportRead();
|
||||
return super.chatNotificationsOnBottom;
|
||||
}
|
||||
|
||||
@override
|
||||
set chatNotificationsOnBottom(bool value) {
|
||||
_$chatNotificationsOnBottomAtom
|
||||
.reportWrite(value, super.chatNotificationsOnBottom, () {
|
||||
super.chatNotificationsOnBottom = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$landscapeChatLeftSideAtom =
|
||||
Atom(name: '_SettingsStoreBase.landscapeChatLeftSide', context: context);
|
||||
|
||||
@override
|
||||
bool get landscapeChatLeftSide {
|
||||
_$landscapeChatLeftSideAtom.reportRead();
|
||||
return super.landscapeChatLeftSide;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeChatLeftSide(bool value) {
|
||||
_$landscapeChatLeftSideAtom.reportWrite(value, super.landscapeChatLeftSide,
|
||||
() {
|
||||
super.landscapeChatLeftSide = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$landscapeForceVerticalChatAtom = Atom(
|
||||
name: '_SettingsStoreBase.landscapeForceVerticalChat', context: context);
|
||||
|
||||
@override
|
||||
bool get landscapeForceVerticalChat {
|
||||
_$landscapeForceVerticalChatAtom.reportRead();
|
||||
return super.landscapeForceVerticalChat;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeForceVerticalChat(bool value) {
|
||||
_$landscapeForceVerticalChatAtom
|
||||
.reportWrite(value, super.landscapeForceVerticalChat, () {
|
||||
super.landscapeForceVerticalChat = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$landscapeCutoutAtom =
|
||||
Atom(name: '_SettingsStoreBase.landscapeCutout', context: context);
|
||||
|
||||
@override
|
||||
LandscapeCutoutType get landscapeCutout {
|
||||
_$landscapeCutoutAtom.reportRead();
|
||||
return super.landscapeCutout;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeCutout(LandscapeCutoutType value) {
|
||||
_$landscapeCutoutAtom.reportWrite(value, super.landscapeCutout, () {
|
||||
super.landscapeCutout = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatWidthAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatWidth', context: context);
|
||||
|
||||
@override
|
||||
double get chatWidth {
|
||||
_$chatWidthAtom.reportRead();
|
||||
return super.chatWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
set chatWidth(double value) {
|
||||
_$chatWidthAtom.reportWrite(value, super.chatWidth, () {
|
||||
super.chatWidth = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$fullScreenChatOverlayOpacityAtom = Atom(
|
||||
name: '_SettingsStoreBase.fullScreenChatOverlayOpacity',
|
||||
context: context);
|
||||
|
||||
@override
|
||||
double get fullScreenChatOverlayOpacity {
|
||||
_$fullScreenChatOverlayOpacityAtom.reportRead();
|
||||
return super.fullScreenChatOverlayOpacity;
|
||||
}
|
||||
|
||||
@override
|
||||
set fullScreenChatOverlayOpacity(double value) {
|
||||
_$fullScreenChatOverlayOpacityAtom
|
||||
.reportWrite(value, super.fullScreenChatOverlayOpacity, () {
|
||||
super.fullScreenChatOverlayOpacity = value;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@override
|
||||
bool get autocomplete {
|
||||
_$autocompleteAtom.reportRead();
|
||||
return super.autocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
set autocomplete(bool value) {
|
||||
_$autocompleteAtom.reportWrite(value, super.autocomplete, () {
|
||||
super.autocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$sendCrashLogsAtom =
|
||||
Atom(name: '_SettingsStoreBase.sendCrashLogs', context: context);
|
||||
|
||||
|
|
@ -660,22 +640,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$expandInfoAtom =
|
||||
Atom(name: '_SettingsStoreBase.expandInfo', context: context);
|
||||
|
||||
@override
|
||||
bool get expandInfo {
|
||||
_$expandInfoAtom.reportRead();
|
||||
return super.expandInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
set expandInfo(bool value) {
|
||||
_$expandInfoAtom.reportWrite(value, super.expandInfo, () {
|
||||
super.expandInfo = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$fullScreenChatOverlayAtom =
|
||||
Atom(name: '_SettingsStoreBase.fullScreenChatOverlay', context: context);
|
||||
|
||||
|
|
@ -768,37 +732,35 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
themeType: ${themeType},
|
||||
showThumbnails: ${showThumbnails},
|
||||
largeStreamCard: ${largeStreamCard},
|
||||
showThumbnailUptime: ${showThumbnailUptime},
|
||||
launchUrlExternal: ${launchUrlExternal},
|
||||
showVideo: ${showVideo},
|
||||
showOverlay: ${showOverlay},
|
||||
toggleableOverlay: ${toggleableOverlay},
|
||||
overlayOpacity: ${overlayOpacity},
|
||||
chatDelay: ${chatDelay},
|
||||
chatOnlyPreventSleep: ${chatOnlyPreventSleep},
|
||||
autocomplete: ${autocomplete},
|
||||
showBottomBar: ${showBottomBar},
|
||||
emoteMenuButtonOnLeft: ${emoteMenuButtonOnLeft},
|
||||
landscapeChatLeftSide: ${landscapeChatLeftSide},
|
||||
landscapeForceVerticalChat: ${landscapeForceVerticalChat},
|
||||
chatNotificationsOnBottom: ${chatNotificationsOnBottom},
|
||||
landscapeCutout: ${landscapeCutout},
|
||||
chatWidth: ${chatWidth},
|
||||
fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity},
|
||||
useReadableColors: ${useReadableColors},
|
||||
showDeletedMessages: ${showDeletedMessages},
|
||||
showChatMessageDividers: ${showChatMessageDividers},
|
||||
timestampType: ${timestampType},
|
||||
highlightFirstTimeChatter: ${highlightFirstTimeChatter},
|
||||
showUserNotices: ${showUserNotices},
|
||||
badgeScale: ${badgeScale},
|
||||
emoteScale: ${emoteScale},
|
||||
messageScale: ${messageScale},
|
||||
messageSpacing: ${messageSpacing},
|
||||
fontSize: ${fontSize},
|
||||
useReadableColors: ${useReadableColors},
|
||||
showDeletedMessages: ${showDeletedMessages},
|
||||
showChatMessageDividers: ${showChatMessageDividers},
|
||||
timestampType: ${timestampType},
|
||||
chatDelay: ${chatDelay},
|
||||
highlightFirstTimeChatter: ${highlightFirstTimeChatter},
|
||||
showUserNotices: ${showUserNotices},
|
||||
showBottomBar: ${showBottomBar},
|
||||
emoteMenuButtonOnLeft: ${emoteMenuButtonOnLeft},
|
||||
chatNotificationsOnBottom: ${chatNotificationsOnBottom},
|
||||
landscapeChatLeftSide: ${landscapeChatLeftSide},
|
||||
landscapeForceVerticalChat: ${landscapeForceVerticalChat},
|
||||
landscapeCutout: ${landscapeCutout},
|
||||
chatWidth: ${chatWidth},
|
||||
fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity},
|
||||
chatOnlyPreventSleep: ${chatOnlyPreventSleep},
|
||||
autocomplete: ${autocomplete},
|
||||
sendCrashLogs: ${sendCrashLogs},
|
||||
fullScreen: ${fullScreen},
|
||||
expandInfo: ${expandInfo},
|
||||
fullScreenChatOverlay: ${fullScreenChatOverlay}
|
||||
''';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_slider.dart';
|
||||
import 'package:frosty/screens/settings/widgets/settings_list_switch.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
||||
class VideoSettings extends StatelessWidget {
|
||||
final SettingsStore settingsStore;
|
||||
|
|
@ -10,58 +13,34 @@ class VideoSettings extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) => ExpansionTile(
|
||||
leading: const Icon(Icons.ondemand_video),
|
||||
title: const Text(
|
||||
'Video',
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
builder: (context) => ListView(
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
title: const Text('Video'),
|
||||
const SectionHeader('Player'),
|
||||
SettingsListSwitch(
|
||||
title: 'Enable video',
|
||||
value: settingsStore.showVideo,
|
||||
onChanged: (newValue) => settingsStore.showVideo = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Custom overlay'),
|
||||
subtitle: const Text('Replaces Twitch\'s default overlay with a mobile-friendly version.'),
|
||||
const SectionHeader('Overlay'),
|
||||
SettingsListSwitch(
|
||||
title: 'Use custom video overlay',
|
||||
subtitle: const Text('Replaces Twitch\'s default web overlay with a mobile-friendly version.'),
|
||||
value: settingsStore.showOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.showOverlay = newValue : null,
|
||||
onChanged: (newValue) => settingsStore.showOverlay = newValue,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Long-press player to toggle overlay'),
|
||||
subtitle: const Text('Allows switching between Twitch\'s default overlay and the custom overlay.'),
|
||||
SettingsListSwitch(
|
||||
title: 'Long-press player to toggle overlay',
|
||||
subtitle: const Text('Allows switching between Twitch\'s overlay and the custom overlay.'),
|
||||
value: settingsStore.toggleableOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.toggleableOverlay = newValue : null,
|
||||
onChanged: (newValue) => settingsStore.toggleableOverlay = newValue,
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Custom overlay opacity'),
|
||||
const Spacer(),
|
||||
Text('${(settingsStore.overlayOpacity * 100).toStringAsFixed(0)}%'),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: settingsStore.overlayOpacity,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
divisions: 10,
|
||||
onChanged: (newValue) => settingsStore.overlayOpacity = newValue,
|
||||
),
|
||||
const Text('Adjusts the opacity (transparency) of the stream overlay when active.'),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
SettingsListSlider(
|
||||
title: 'Custom overlay opacity',
|
||||
trailing: '${(settingsStore.overlayOpacity * 100).toStringAsFixed(0)}%',
|
||||
subtitle: 'Adjusts the opacity (transparency) of the custom video overlay when active.',
|
||||
value: settingsStore.overlayOpacity,
|
||||
divisions: 10,
|
||||
onChanged: (newValue) => settingsStore.overlayOpacity = newValue,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
72
lib/screens/settings/widgets/settings_list_select.dart
Normal file
72
lib/screens/settings/widgets/settings_list_select.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/widgets/bottom_sheet.dart';
|
||||
|
||||
/// A custom-styled adaptive [ListTile] with options to select.
|
||||
class SettingsListSelect extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String selectedOption;
|
||||
final List<String> options;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
const SettingsListSelect({
|
||||
Key? key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.selectedOption,
|
||||
required this.options,
|
||||
required this.onChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
isThreeLine: subtitle != null,
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||
trailing: SizedBox(
|
||||
height: double.infinity,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.8,
|
||||
child: Text(
|
||||
selectedOption,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () => showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => FrostyBottomSheet(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Select ${title.toLowerCase()}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
),
|
||||
...options
|
||||
.map((option) => ListTile(
|
||||
title: Text(option),
|
||||
trailing: selectedOption == option ? const Icon(Icons.check_rounded) : null,
|
||||
onTap: () {
|
||||
onChanged(option);
|
||||
Navigator.of(context).pop();
|
||||
}))
|
||||
.toList()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/screens/settings/widgets/settings_list_slider.dart
Normal file
56
lib/screens/settings/widgets/settings_list_slider.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A custom-styled adaptive [Slider].
|
||||
class SettingsListSlider extends StatelessWidget {
|
||||
final String title;
|
||||
final String trailing;
|
||||
final String? subtitle;
|
||||
final double value;
|
||||
final ValueChanged<double>? onChanged;
|
||||
final double min;
|
||||
final double max;
|
||||
final int? divisions;
|
||||
|
||||
const SettingsListSlider({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.trailing,
|
||||
this.subtitle,
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
this.min = 0.0,
|
||||
this.max = 1.0,
|
||||
this.divisions,
|
||||
}) : super(key: key);
|
||||
|
||||
static const _textStyle = TextStyle(fontWeight: FontWeight.w500);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(title, style: _textStyle),
|
||||
const Spacer(),
|
||||
Text(trailing, style: _textStyle.copyWith(fontFeatures: [const FontFeature.tabularFigures()])),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Slider.adaptive(
|
||||
value: value,
|
||||
min: min,
|
||||
max: max,
|
||||
divisions: divisions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
if (subtitle != null) Text(subtitle!),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/screens/settings/widgets/settings_list_switch.dart
Normal file
28
lib/screens/settings/widgets/settings_list_switch.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A custom-styled adaptive [SwitchListTile].
|
||||
class SettingsListSwitch extends StatelessWidget {
|
||||
final String? title;
|
||||
final Widget? subtitle;
|
||||
final bool value;
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
const SettingsListSwitch({
|
||||
Key? key,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.onChanged,
|
||||
required this.value,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile.adaptive(
|
||||
isThreeLine: subtitle != null,
|
||||
title: Text(title!, style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||
subtitle: subtitle,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/screens/settings/widgets/settings_tile_route.dart
Normal file
35
lib/screens/settings/widgets/settings_tile_route.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
|
||||
class SettingsTileRoute extends StatelessWidget {
|
||||
final Widget leading;
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
const SettingsTileRoute({
|
||||
Key? key,
|
||||
required this.leading,
|
||||
required this.title,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: leading,
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: FrostyAppBar(title: Text(title)),
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,13 +3,11 @@ import 'package:flutter/material.dart';
|
|||
/// A simple widget that displays an alert message in the center.
|
||||
class AlertMessage extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final Color? color;
|
||||
|
||||
const AlertMessage({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.icon = Icons.info,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
|
|
@ -19,7 +17,7 @@ class AlertMessage extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
Icons.info_outline_rounded,
|
||||
color: color ?? Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ class _AnimateScaleState extends State<AnimateScale> with SingleTickerProviderSt
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const duration = Duration(milliseconds: 200);
|
||||
const downCurve = Curves.easeOutCubic;
|
||||
const upCurve = Curves.easeInCubic;
|
||||
const downCurve = Curves.easeOut;
|
||||
const upCurve = Curves.easeIn;
|
||||
|
||||
return InkWell(
|
||||
onTap: widget.onTap,
|
||||
|
|
|
|||
31
lib/widgets/app_bar.dart
Normal file
31
lib/widgets/app_bar.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FrostyAppBar extends StatelessWidget with PreferredSizeWidget {
|
||||
final Widget title;
|
||||
final bool? centerTitle;
|
||||
final List<Widget>? actions;
|
||||
|
||||
const FrostyAppBar({
|
||||
Key? key,
|
||||
required this.title,
|
||||
this.centerTitle,
|
||||
this.actions,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: Icon(Icons.adaptive.arrow_back_rounded),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
title: title,
|
||||
centerTitle: centerTitle,
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
|
||||
class BlockButton extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
final String targetUser;
|
||||
final String targetUserId;
|
||||
final bool simple;
|
||||
|
||||
const BlockButton({
|
||||
Key? key,
|
||||
required this.authStore,
|
||||
required this.targetUser,
|
||||
required this.targetUserId,
|
||||
this.simple = true,
|
||||
}) : super(key: key);
|
||||
|
||||
Future<void> _showDialog(
|
||||
BuildContext context, {
|
||||
required bool isBlocked,
|
||||
required String targetUser,
|
||||
required String targetUserId,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => FrostyDialog(
|
||||
title: isBlocked ? 'Unblock' : 'Block',
|
||||
content: Text(
|
||||
'Are you sure you want to ${isBlocked ? 'unblock $targetUser?' : 'block $targetUser? This will remove them from channel lists, search results, and chat messages.'}'),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () {
|
||||
if (isBlocked) {
|
||||
authStore.user.unblock(targetId: targetUserId, headers: authStore.headersTwitch);
|
||||
} else {
|
||||
authStore.user.block(
|
||||
targetId: targetUserId,
|
||||
displayName: targetUser,
|
||||
headers: authStore.headersTwitch,
|
||||
);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
Button(
|
||||
fill: true,
|
||||
onPressed: Navigator.of(context).pop,
|
||||
color: Colors.red.shade700,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final isBlocked = authStore.user.blockedUsers.where((blockedUser) => blockedUser.userId == targetUserId).isNotEmpty;
|
||||
|
||||
return Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
|
||||
icon: const Icon(Icons.block),
|
||||
color: Colors.red.shade700,
|
||||
fill: true,
|
||||
onPressed: () => _showDialog(
|
||||
context,
|
||||
isBlocked: isBlocked,
|
||||
targetUser: targetUser,
|
||||
targetUserId: targetUserId,
|
||||
),
|
||||
child: isBlocked ? Text(simple ? 'Unblock' : 'Unblock $targetUser') : Text(simple ? 'Block' : 'Block $targetUser'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/block_button.dart';
|
||||
import 'package:frosty/widgets/modal.dart';
|
||||
import 'package:frosty/widgets/report_button.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
import 'package:frosty/widgets/bottom_sheet.dart';
|
||||
import 'package:frosty/widgets/list_tile.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class BlockReportModal extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
|
@ -22,31 +21,38 @@ class BlockReportModal extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FrostyModal(
|
||||
return FrostyBottomSheet(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (authStore.isLoggedIn)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
width: double.infinity,
|
||||
child: BlockButton(
|
||||
authStore: authStore,
|
||||
targetUser: name,
|
||||
targetUserId: userId,
|
||||
simple: false,
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.block_rounded),
|
||||
onTap: () => authStore
|
||||
.showBlockDialog(context, targetUser: name, targetUserId: userId)
|
||||
.then((_) => Navigator.pop(context)),
|
||||
title: 'Block $name',
|
||||
),
|
||||
FrostyListTile(
|
||||
leading: const Icon(Icons.outlined_flag_rounded),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: FrostyAppBar(
|
||||
title: Text('Report $name'),
|
||||
),
|
||||
body: WebView(
|
||||
initialUrl: 'https://www.twitch.tv/$userLogin/report',
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
width: double.infinity,
|
||||
child: ReportButton(
|
||||
userLogin: userLogin,
|
||||
displayName: name,
|
||||
),
|
||||
),
|
||||
if (Platform.isAndroid) const SizedBox(height: 20.0),
|
||||
title: 'Report $name',
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FrostyModal extends StatelessWidget {
|
||||
class FrostyBottomSheet extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const FrostyModal({Key? key, required this.child}) : super(key: key);
|
||||
const FrostyBottomSheet({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dialogBackgroundColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20.0),
|
||||
topRight: Radius.circular(20.0),
|
||||
topLeft: Radius.circular(15.0),
|
||||
topRight: Radius.circular(15.0),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
|
|
@ -21,11 +20,11 @@ class FrostyModal extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FractionallySizedBox(
|
||||
widthFactor: 0.25,
|
||||
widthFactor: 0.2,
|
||||
child: SizedBox(
|
||||
child: Divider(
|
||||
height: 25.0,
|
||||
thickness: 3.0,
|
||||
height: 30.0,
|
||||
thickness: 2.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -40,8 +40,8 @@ class _ButtonState extends State<Button> with SingleTickerProviderStateMixin {
|
|||
backgroundColor: widget.color == null || widget.fill ? widget.color : Colors.transparent,
|
||||
padding: widget.padding,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
textStyle: TextStyle(fontWeight: FontWeight.w600, fontSize: widget.fontSize),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30.0)),
|
||||
textStyle: DefaultTextStyle.of(context).style.copyWith(fontWeight: FontWeight.w600, fontSize: widget.fontSize),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
|
||||
elevation: widget.color == null ? 10.0 : 0.0,
|
||||
).copyWith(
|
||||
elevation: (widget.color == null || widget.fill) && widget.onPressed != null
|
||||
|
|
@ -81,7 +81,7 @@ class _ButtonState extends State<Button> with SingleTickerProviderStateMixin {
|
|||
if (mounted) {
|
||||
_animationController.animateTo(
|
||||
_animationController.upperBound,
|
||||
curve: Curves.easeOutCubic,
|
||||
curve: Curves.easeOut,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ class _ButtonState extends State<Button> with SingleTickerProviderStateMixin {
|
|||
if (mounted) {
|
||||
_animationController.animateTo(
|
||||
_animationController.lowerBound,
|
||||
curve: Curves.easeInCubic,
|
||||
curve: Curves.easeIn,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ class _ButtonState extends State<Button> with SingleTickerProviderStateMixin {
|
|||
if (mounted) {
|
||||
_animationController.animateTo(
|
||||
_animationController.lowerBound,
|
||||
curve: Curves.easeInCubic,
|
||||
curve: Curves.easeIn,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
44
lib/widgets/cached_image.dart
Normal file
44
lib/widgets/cached_image.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A wrapper around [CachedNetworkImage] that adds custom defaults for Frosty.
|
||||
class FrostyCachedNetworkImage extends StatelessWidget {
|
||||
final String imageUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Color? color;
|
||||
final BlendMode? colorBlendMode;
|
||||
final Widget Function(BuildContext, String)? placeholder;
|
||||
final bool useFade;
|
||||
final BoxFit? fit;
|
||||
|
||||
const FrostyCachedNetworkImage({
|
||||
Key? key,
|
||||
required this.imageUrl,
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.colorBlendMode,
|
||||
this.placeholder,
|
||||
this.useFade = true,
|
||||
this.fit,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
colorBlendMode: colorBlendMode,
|
||||
placeholder: placeholder,
|
||||
useOldImageOnUrlChange: true,
|
||||
fadeOutDuration: useFade ? const Duration(milliseconds: 500) : Duration.zero,
|
||||
fadeInDuration: useFade ? const Duration(milliseconds: 500) : Duration.zero,
|
||||
fadeInCurve: Curves.easeOut,
|
||||
fadeOutCurve: Curves.easeIn,
|
||||
fit: fit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,37 +3,40 @@ import 'package:flutter/material.dart';
|
|||
/// A custom dialog that makes the title bold, puts the actions in a column, and adds a subtle border.
|
||||
class FrostyDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget content;
|
||||
final String? message;
|
||||
final Widget? content;
|
||||
final List<Widget>? actions;
|
||||
|
||||
const FrostyDialog({
|
||||
Key? key,
|
||||
this.actions,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.message,
|
||||
this.content,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(30.0, 30.0, 30.0, 20.0),
|
||||
contentPadding: const EdgeInsets.fromLTRB(30.0, 0.0, 30.0, 30.0),
|
||||
titlePadding: const EdgeInsets.fromLTRB(20.0, 30.0, 20.0, 20.0),
|
||||
contentPadding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(children: [
|
||||
content,
|
||||
content ?? Text(message!, textAlign: TextAlign.center),
|
||||
if (actions != null) ...[
|
||||
const SizedBox(height: 20.0),
|
||||
const SizedBox(height: 25.0),
|
||||
...?actions?.map(
|
||||
(action) => Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(top: 10.0),
|
||||
padding: const EdgeInsets.only(top: 5.0),
|
||||
child: action,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
32
lib/widgets/list_tile.dart
Normal file
32
lib/widgets/list_tile.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class FrostyListTile extends StatelessWidget {
|
||||
final bool? isThreeLine;
|
||||
final Widget? leading;
|
||||
final String title;
|
||||
final Widget? subtitle;
|
||||
final Widget? trailing;
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
const FrostyListTile({
|
||||
Key? key,
|
||||
this.isThreeLine,
|
||||
this.leading,
|
||||
required this.title,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.subtitle,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
isThreeLine: isThreeLine ?? subtitle != null,
|
||||
leading: leading,
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
|
||||
class FrostyNotification extends StatelessWidget {
|
||||
|
|
@ -17,8 +18,8 @@ class FrostyNotification extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
color: lightGray,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
margin: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
|
|
@ -29,7 +30,7 @@ class FrostyNotification extends StatelessWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info,
|
||||
Icons.info_outline_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
|
@ -50,7 +51,7 @@ class FrostyNotification extends StatelessWidget {
|
|||
if (showPasteButton)
|
||||
Button(
|
||||
onPressed: onButtonPressed,
|
||||
fill: false,
|
||||
color: purple,
|
||||
child: const Text('Paste'),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
79
lib/widgets/page_view.dart
Normal file
79
lib/widgets/page_view.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
|
||||
class FrostyPageView extends StatefulWidget {
|
||||
final List<String> headers;
|
||||
final List<Widget> children;
|
||||
|
||||
const FrostyPageView({
|
||||
Key? key,
|
||||
required this.headers,
|
||||
required this.children,
|
||||
}) : assert(headers.length == children.length),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<FrostyPageView> createState() => _FrostyPageViewState();
|
||||
}
|
||||
|
||||
class _FrostyPageViewState extends State<FrostyPageView> {
|
||||
var currentIndex = 0;
|
||||
|
||||
late final PageController _pageContoller = PageController(initialPage: currentIndex);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(
|
||||
height: 5.0,
|
||||
thickness: 1.0,
|
||||
),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
...widget.headers.mapIndexed(
|
||||
(index, section) => SizedBox(
|
||||
height: 40,
|
||||
child: Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
||||
onPressed: () {
|
||||
setState(() => currentIndex = index);
|
||||
|
||||
_pageContoller.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
},
|
||||
color: index == currentIndex ? Theme.of(context).colorScheme.secondary : Colors.grey,
|
||||
child: Text(
|
||||
section,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageContoller,
|
||||
itemBuilder: (context, index) => widget.children[index],
|
||||
itemCount: widget.children.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageContoller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/models/user.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/widgets/cached_image.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ProfilePicture extends StatelessWidget {
|
||||
|
|
@ -21,18 +22,26 @@ class ProfilePicture extends StatelessWidget {
|
|||
|
||||
return ClipOval(
|
||||
child: FutureBuilder(
|
||||
future: context.read<TwitchApi>().getUser(userLogin: userLogin, headers: context.read<AuthStore>().headersTwitch),
|
||||
future:
|
||||
context.read<TwitchApi>().getUser(userLogin: userLogin, headers: context.read<AuthStore>().headersTwitch),
|
||||
builder: (context, AsyncSnapshot<UserTwitch> snapshot) {
|
||||
return snapshot.hasData
|
||||
? CachedNetworkImage(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
imageUrl: snapshot.data!.profileImageUrl,
|
||||
)
|
||||
: SizedBox(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
);
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: snapshot.hasData
|
||||
? FrostyCachedNetworkImage(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
imageUrl: snapshot.data!.profileImageUrl,
|
||||
placeholder: (context, url) => const ColoredBox(color: lightGray),
|
||||
)
|
||||
: Container(
|
||||
color: lightGray,
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class ReportButton extends StatelessWidget {
|
||||
final String userLogin;
|
||||
final String displayName;
|
||||
|
||||
const ReportButton({
|
||||
Key? key,
|
||||
required this.userLogin,
|
||||
required this.displayName,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Button(
|
||||
icon: const Icon(Icons.report),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Report'),
|
||||
),
|
||||
body: WebView(
|
||||
initialUrl: 'https://www.twitch.tv/$userLogin/report',
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
|
||||
color: Colors.red.shade700,
|
||||
fill: true,
|
||||
child: Text('Report $displayName'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,11 +16,11 @@ class ScrollToTopButton extends StatelessWidget {
|
|||
child: Button(
|
||||
onPressed: () => scrollController.animateTo(
|
||||
0.0,
|
||||
curve: Curves.easeOutCubic,
|
||||
curve: Curves.easeOut,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25.0, vertical: 10.0),
|
||||
child: const Icon(Icons.keyboard_arrow_up),
|
||||
child: const Icon(Icons.keyboard_double_arrow_up_rounded),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,40 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
|
||||
class SectionHeader extends StatelessWidget {
|
||||
final String text;
|
||||
final EdgeInsets? padding;
|
||||
final double? fontSize;
|
||||
final bool showDivider;
|
||||
|
||||
const SectionHeader(
|
||||
this.text, {
|
||||
Key? key,
|
||||
this.padding,
|
||||
this.fontSize,
|
||||
this.showDivider = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding ?? const EdgeInsets.fromLTRB(10.0, 20.0, 10.0, 5.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
text.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.8,
|
||||
),
|
||||
),
|
||||
if (showDivider)
|
||||
const Divider(
|
||||
height: 10.0,
|
||||
thickness: 1.0,
|
||||
),
|
||||
],
|
||||
padding: padding ?? const EdgeInsets.fromLTRB(16, 30, 16, 5),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: purple,
|
||||
fontSize: fontSize ?? 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
14
pubspec.lock
14
pubspec.lock
|
|
@ -392,13 +392,6 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
font_awesome_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: font_awesome_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "10.2.1"
|
||||
freezed_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -854,6 +847,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
simple_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: simple_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "7.10.0"
|
||||
simple_pip_mode:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -49,13 +49,13 @@ dependencies:
|
|||
tuple: ^2.0.0
|
||||
flutter_svg: ^1.0.0
|
||||
url_launcher: ^6.0.17
|
||||
font_awesome_flutter: ^10.0.0
|
||||
package_info_plus: ^1.3.0
|
||||
collection: ^1.16.0
|
||||
flutter_cache_manager: ^3.3.0
|
||||
device_info_plus: ^4.0.0
|
||||
wakelock: ^0.6.2
|
||||
device_preview: ^1.1.0
|
||||
simple_icons: ^7.10.0
|
||||
simple_pip_mode:
|
||||
git:
|
||||
url: https://github.com/tommyxchow/simple_pip_mode_flutter
|
||||
|
|
@ -107,7 +107,6 @@ flutter:
|
|||
|
||||
assets:
|
||||
- assets/icons/logo.svg
|
||||
- assets/icons/TwitchGlitchWhite.png
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue