commit
2058b7da42
29 changed files with 872 additions and 615 deletions
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
|
|
@ -14,8 +14,8 @@ jobs:
|
|||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: subosito/flutter-action@v2
|
||||
|
||||
- run: flutter doctor -v
|
||||
- run: flutter pub get
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 244 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 244 KiB |
|
|
@ -1,4 +1,6 @@
|
|||
PODS:
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
|
|
@ -31,6 +33,7 @@ PODS:
|
|||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
|
|
@ -49,6 +52,8 @@ SPEC REPOS:
|
|||
- Sentry
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_native_splash:
|
||||
|
|
@ -73,6 +78,7 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
|
||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 244 KiB |
|
|
@ -5,48 +5,15 @@
|
|||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "LaunchImageDark.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "LaunchImageDark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "LaunchImageDark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
|
|
@ -18,7 +18,6 @@
|
|||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="BrandingImage" translatesAutoresizingMaskIntoConstraints="NO" id="Uyq-Kz-ftE"/>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
|
|
@ -41,6 +40,5 @@
|
|||
<resources>
|
||||
<image name="LaunchImage" width="757" height="358"/>
|
||||
<image name="LaunchBackground" width="1" height="1"/>
|
||||
<image name="BrandingImage" width="1" height="1"/>
|
||||
</resources>
|
||||
</document>
|
||||
150
lib/main.dart
150
lib/main.dart
|
|
@ -75,86 +75,86 @@ void main() async {
|
|||
// Navigator key for sleep timer. Allows navigation popping without context.
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
const gray = Color.fromRGBO(22, 22, 22, 1.0);
|
||||
const purple = Color(0xff9146ff);
|
||||
|
||||
final lightTheme = ThemeData(
|
||||
canvasColor: Colors.white,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
fontFamily: 'Inter',
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.white,
|
||||
elevation: 0.0,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: purple,
|
||||
),
|
||||
toggleableActiveColor: purple,
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
),
|
||||
);
|
||||
|
||||
final darkTheme = ThemeData(
|
||||
canvasColor: gray,
|
||||
brightness: Brightness.dark,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
fontFamily: 'Inter',
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: gray,
|
||||
elevation: 0.0,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(backgroundColor: gray),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: purple,
|
||||
),
|
||||
dialogBackgroundColor: gray,
|
||||
toggleableActiveColor: purple,
|
||||
);
|
||||
|
||||
final oledTheme = ThemeData(
|
||||
canvasColor: Colors.black,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
fontFamily: 'Inter',
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.black,
|
||||
elevation: 0.0,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: purple,
|
||||
),
|
||||
dialogBackgroundColor: Colors.black,
|
||||
toggleableActiveColor: purple,
|
||||
);
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const gray = Color.fromRGBO(22, 22, 22, 1.0);
|
||||
const purple = Color(0xff9146ff);
|
||||
|
||||
final lightTheme = ThemeData(
|
||||
canvasColor: Colors.white,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
fontFamily: 'Inter',
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.white,
|
||||
elevation: 0.0,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: purple,
|
||||
),
|
||||
toggleableActiveColor: purple,
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
),
|
||||
);
|
||||
|
||||
final darkTheme = ThemeData(
|
||||
canvasColor: gray,
|
||||
brightness: Brightness.dark,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
fontFamily: 'Inter',
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: gray,
|
||||
elevation: 0.0,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(backgroundColor: gray),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: purple,
|
||||
),
|
||||
dialogBackgroundColor: gray,
|
||||
toggleableActiveColor: purple,
|
||||
);
|
||||
|
||||
final oledTheme = ThemeData(
|
||||
canvasColor: Colors.black,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
fontFamily: 'Inter',
|
||||
appBarTheme: const AppBarTheme(
|
||||
color: Colors.black,
|
||||
elevation: 0.0,
|
||||
titleTextStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.deepPurple,
|
||||
accentColor: purple,
|
||||
),
|
||||
dialogBackgroundColor: Colors.black,
|
||||
toggleableActiveColor: purple,
|
||||
);
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final settingsStore = context.read<SettingsStore>();
|
||||
|
|
|
|||
|
|
@ -108,8 +108,7 @@ class Chat extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (chatStore.settings.showBottomBar && (!chatStore.settings.fullScreen || MediaQuery.of(context).orientation == Orientation.portrait))
|
||||
ChatBottomBar(chatStore: chatStore),
|
||||
if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore),
|
||||
if (chatStore.assetsStore.showEmoteMenu)
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height / 3,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class _ChattersListState extends State<ChattersList> {
|
|||
widget.chatDetails.showJumpButton = true;
|
||||
}
|
||||
});
|
||||
|
||||
widget.chatDetails.updateChatters(widget.userLogin);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -58,7 +60,6 @@ class _ChattersListState extends State<ChattersList> {
|
|||
];
|
||||
|
||||
final chatDetailsStore = widget.chatDetails;
|
||||
chatDetailsStore.updateChatters(widget.userLogin);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
|
@ -116,6 +117,7 @@ class _ChattersListState extends State<ChattersList> {
|
|||
}
|
||||
|
||||
return CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
|
|
|
|||
|
|
@ -14,16 +14,20 @@ class ChatBottomBar extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final emotes = [
|
||||
final matchingEmotes = [
|
||||
...chatStore.assetsStore.userEmoteToObject.values,
|
||||
...chatStore.assetsStore.bttvEmotes,
|
||||
...chatStore.assetsStore.ffzEmotes,
|
||||
...chatStore.assetsStore.sevenTVEmotes
|
||||
].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()))
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (chatStore.settings.emoteAutocomplete && chatStore.showAutocomplete && emotes.isNotEmpty) ...[
|
||||
if (chatStore.settings.autocomplete && chatStore.showEmoteAutocomplete && matchingEmotes.isNotEmpty) ...[
|
||||
const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
|
|
@ -32,27 +36,52 @@ class ChatBottomBar extends StatelessWidget {
|
|||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
itemCount: emotes.length,
|
||||
itemCount: matchingEmotes.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(emotes[index], autocompleteMode: true),
|
||||
onTap: () => chatStore.addEmote(matchingEmotes[index], autocompleteMode: true),
|
||||
child: Tooltip(
|
||||
message: emotes[index].name,
|
||||
message: matchingEmotes[index].name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: emotes[index].url,
|
||||
imageUrl: matchingEmotes[index].url,
|
||||
fadeInDuration: const Duration(),
|
||||
height: emotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emotes[index].width?.toDouble(),
|
||||
height: matchingEmotes[index].height?.toDouble() ?? defaultEmoteSize,
|
||||
width: matchingEmotes[index].width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (chatStore.settings.autocomplete && chatStore.showMentionAutocomplete && matchingChatters.isNotEmpty) ...[
|
||||
const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
||||
itemCount: matchingChatters.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => TextButton(
|
||||
onPressed: () {
|
||||
final split = chatStore.textController.text.split(' ')
|
||||
..removeLast()
|
||||
..add('@${matchingChatters[index]} ');
|
||||
|
||||
chatStore.textController.text = split.join(' ');
|
||||
chatStore.textController.selection = TextSelection.fromPosition(TextPosition(offset: chatStore.textController.text.length));
|
||||
},
|
||||
child: Text(matchingChatters[index]),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
Row(
|
||||
|
|
@ -73,9 +102,7 @@ class ChatBottomBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!chatStore.expandChat &&
|
||||
chatStore.settings.landscapeChatWidth < 0.3 &&
|
||||
MediaQuery.of(context).orientation == Orientation.landscape)
|
||||
if (!chatStore.expandChat && chatStore.settings.chatWidth < 0.3 && MediaQuery.of(context).orientation == Orientation.landscape)
|
||||
IconButton(
|
||||
tooltip: 'Send a Message',
|
||||
onPressed: () {
|
||||
|
|
@ -115,7 +142,7 @@ class ChatBottomBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (chatStore.settings.landscapeChatWidth > 0.3 || chatStore.expandChat || MediaQuery.of(context).orientation == Orientation.portrait)
|
||||
if (chatStore.settings.chatWidth > 0.3 || chatStore.expandChat || MediaQuery.of(context).orientation == Orientation.portrait)
|
||||
IconButton(
|
||||
tooltip: 'Send',
|
||||
icon: const Icon(Icons.send),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ abstract class ChatDetailsStoreBase with Store {
|
|||
@observable
|
||||
var filterText = '';
|
||||
|
||||
/// The list and types of chatters in the chat room.
|
||||
@readonly
|
||||
ChatUsers? _chatUsers;
|
||||
|
||||
@computed
|
||||
Iterable<List<String>> get filteredUsers => [
|
||||
_chatUsers!.chatters.broadcaster,
|
||||
|
|
@ -34,9 +38,16 @@ abstract class ChatDetailsStoreBase with Store {
|
|||
_chatUsers!.chatters.viewers,
|
||||
].map((e) => e.where((user) => user.contains(filterText)).toList());
|
||||
|
||||
/// The list and types of chatters in the chat room.
|
||||
@readonly
|
||||
ChatUsers? _chatUsers;
|
||||
@computed
|
||||
List<String> get allChatters => [
|
||||
...?_chatUsers?.chatters.broadcaster,
|
||||
...?_chatUsers?.chatters.staff,
|
||||
...?_chatUsers?.chatters.admins,
|
||||
...?_chatUsers?.chatters.globalMods,
|
||||
...?_chatUsers?.chatters.moderators,
|
||||
...?_chatUsers?.chatters.vips,
|
||||
...?_chatUsers?.chatters.viewers,
|
||||
];
|
||||
|
||||
@readonly
|
||||
String? _error;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ mixin _$ChatDetailsStore on ChatDetailsStoreBase, Store {
|
|||
Computed<Iterable<List<String>>>(() => super.filteredUsers,
|
||||
name: 'ChatDetailsStoreBase.filteredUsers'))
|
||||
.value;
|
||||
Computed<List<String>>? _$allChattersComputed;
|
||||
|
||||
@override
|
||||
List<String> get allChatters =>
|
||||
(_$allChattersComputed ??= Computed<List<String>>(() => super.allChatters,
|
||||
name: 'ChatDetailsStoreBase.allChatters'))
|
||||
.value;
|
||||
|
||||
late final _$roomStateAtom =
|
||||
Atom(name: 'ChatDetailsStoreBase.roomState', context: context);
|
||||
|
|
@ -116,7 +123,8 @@ mixin _$ChatDetailsStore on ChatDetailsStoreBase, Store {
|
|||
roomState: ${roomState},
|
||||
showJumpButton: ${showJumpButton},
|
||||
filterText: ${filterText},
|
||||
filteredUsers: ${filteredUsers}
|
||||
filteredUsers: ${filteredUsers},
|
||||
allChatters: ${allChatters}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,10 @@ abstract class ChatStoreBase with Store {
|
|||
var _autoScroll = true;
|
||||
|
||||
@readonly
|
||||
var _showAutocomplete = false;
|
||||
var _showEmoteAutocomplete = false;
|
||||
|
||||
@readonly
|
||||
var _showMentionAutocomplete = false;
|
||||
|
||||
/// The logged-in user's appearance in chat.
|
||||
@readonly
|
||||
|
|
@ -100,6 +103,7 @@ abstract class ChatStoreBase with Store {
|
|||
));
|
||||
|
||||
assetsStore.init();
|
||||
chatDetailsStore.updateChatters(channelName);
|
||||
|
||||
_messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
|
||||
|
|
@ -121,17 +125,25 @@ abstract class ChatStoreBase with Store {
|
|||
}
|
||||
});
|
||||
|
||||
// Add a listener to the textfield focus that will hide the emote menu if it is currently shown.
|
||||
textFieldFocusNode.addListener(() {
|
||||
if (textFieldFocusNode.hasFocus && assetsStore.showEmoteMenu) assetsStore.showEmoteMenu = false;
|
||||
if (textFieldFocusNode.hasFocus) {
|
||||
// Hide the emote menu if it is currently shown.
|
||||
if (assetsStore.showEmoteMenu) assetsStore.showEmoteMenu = false;
|
||||
|
||||
// Refresh the chatters list to keep autocomplete mentions updated.
|
||||
if (settings.autocomplete) chatDetailsStore.updateChatters(channelName);
|
||||
}
|
||||
|
||||
// Un-expand the chat when unfocusing.
|
||||
if (!textFieldFocusNode.hasFocus) expandChat = false;
|
||||
});
|
||||
|
||||
// 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(() => _showAutocomplete = textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty ? true : false);
|
||||
textController.addListener(
|
||||
() => _showEmoteAutocomplete = !_showMentionAutocomplete && textFieldFocusNode.hasFocus && textController.text.split(' ').last.isNotEmpty);
|
||||
|
||||
textController.addListener(() => _showMentionAutocomplete = textFieldFocusNode.hasFocus && textController.text.split(' ').last.startsWith('@'));
|
||||
}
|
||||
|
||||
/// Handle and process the provided string-representation of the IRC data.
|
||||
|
|
@ -342,7 +354,7 @@ abstract class ChatStoreBase with Store {
|
|||
void addEmote(Emote emote, {bool autocompleteMode = false}) {
|
||||
if (textController.text.isEmpty || textController.text.endsWith(' ')) {
|
||||
textController.text += '${emote.name} ';
|
||||
} else if (autocompleteMode && _showAutocomplete && textController.text.endsWith('')) {
|
||||
} else if (autocompleteMode && _showEmoteAutocomplete && textController.text.endsWith('')) {
|
||||
final split = textController.text.split(' ')
|
||||
..removeLast()
|
||||
..add('${emote.name} ');
|
||||
|
|
|
|||
|
|
@ -45,21 +45,41 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_showAutocompleteAtom =
|
||||
Atom(name: 'ChatStoreBase._showAutocomplete', context: context);
|
||||
late final _$_showEmoteAutocompleteAtom =
|
||||
Atom(name: 'ChatStoreBase._showEmoteAutocomplete', context: context);
|
||||
|
||||
bool get showAutocomplete {
|
||||
_$_showAutocompleteAtom.reportRead();
|
||||
return super._showAutocomplete;
|
||||
bool get showEmoteAutocomplete {
|
||||
_$_showEmoteAutocompleteAtom.reportRead();
|
||||
return super._showEmoteAutocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _showAutocomplete => showAutocomplete;
|
||||
bool get _showEmoteAutocomplete => showEmoteAutocomplete;
|
||||
|
||||
@override
|
||||
set _showAutocomplete(bool value) {
|
||||
_$_showAutocompleteAtom.reportWrite(value, super._showAutocomplete, () {
|
||||
super._showAutocomplete = value;
|
||||
set _showEmoteAutocomplete(bool value) {
|
||||
_$_showEmoteAutocompleteAtom
|
||||
.reportWrite(value, super._showEmoteAutocomplete, () {
|
||||
super._showEmoteAutocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_showMentionAutocompleteAtom =
|
||||
Atom(name: 'ChatStoreBase._showMentionAutocomplete', context: context);
|
||||
|
||||
bool get showMentionAutocomplete {
|
||||
_$_showMentionAutocompleteAtom.reportRead();
|
||||
return super._showMentionAutocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _showMentionAutocomplete => showMentionAutocomplete;
|
||||
|
||||
@override
|
||||
set _showMentionAutocomplete(bool value) {
|
||||
_$_showMentionAutocompleteAtom
|
||||
.reportWrite(value, super._showMentionAutocomplete, () {
|
||||
super._showMentionAutocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/api/twitch_api.dart';
|
||||
|
|
@ -37,6 +39,9 @@ abstract class VideoStoreBase with Store {
|
|||
@readonly
|
||||
var _overlayVisible = true;
|
||||
|
||||
@readonly
|
||||
var _isIPad = false;
|
||||
|
||||
@readonly
|
||||
StreamTwitch? _streamInfo;
|
||||
|
||||
|
|
@ -152,13 +157,27 @@ abstract class VideoStoreBase with Store {
|
|||
return NavigationDecision.prevent;
|
||||
}
|
||||
|
||||
void initVideo() {
|
||||
@action
|
||||
Future<void> initVideo() async {
|
||||
try {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("pause", () => Pause.postMessage("video paused"));');
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("play", () => Play.postMessage("video playing"));');
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
|
||||
// Determine whether the device is an iPad or not.
|
||||
// Used to show or hide the rotate button on the overlay.
|
||||
// Flutter doesn't allow programmatic rotation on iPad unless multitasking is disabled.
|
||||
if (Platform.isIOS) {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final info = await deviceInfo.iosInfo;
|
||||
if (info.model?.toLowerCase().contains('ipad') == true) {
|
||||
_isIPad = true;
|
||||
} else {
|
||||
_isIPad = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void requestPictureInPicture() {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,24 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_isIPadAtom =
|
||||
Atom(name: 'VideoStoreBase._isIPad', context: context);
|
||||
|
||||
bool get isIPad {
|
||||
_$_isIPadAtom.reportRead();
|
||||
return super._isIPad;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _isIPad => isIPad;
|
||||
|
||||
@override
|
||||
set _isIPad(bool value) {
|
||||
_$_isIPadAtom.reportWrite(value, super._isIPad, () {
|
||||
super._isIPad = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_streamInfoAtom =
|
||||
Atom(name: 'VideoStoreBase._streamInfo', context: context);
|
||||
|
||||
|
|
@ -136,6 +154,14 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
.run(() => super.handleToggleOverlay());
|
||||
}
|
||||
|
||||
late final _$initVideoAsyncAction =
|
||||
AsyncAction('VideoStoreBase.initVideo', context: context);
|
||||
|
||||
@override
|
||||
Future<void> initVideo() {
|
||||
return _$initVideoAsyncAction.run(() => super.initVideo());
|
||||
}
|
||||
|
||||
late final _$VideoStoreBaseActionController =
|
||||
ActionController(name: 'VideoStoreBase', context: context);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/screens/channel/stores/video_store.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class Video extends StatelessWidget {
|
||||
final VideoStore videoStore;
|
||||
|
||||
const Video({
|
||||
Key? key,
|
||||
required this.videoStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebView(
|
||||
backgroundColor: Colors.black,
|
||||
initialUrl: videoStore.videoUrl,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
allowsInlineMediaPlayback: true,
|
||||
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
||||
onWebViewCreated: (controller) => videoStore.controller = controller,
|
||||
onPageFinished: (string) => videoStore.initVideo(),
|
||||
navigationDelegate: videoStore.handleNavigation,
|
||||
javascriptChannels: videoStore.javascriptChannels,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/screens/channel/stores/video_store.dart';
|
||||
import 'package:frosty/screens/settings/settings.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class VideoOverlay extends StatelessWidget {
|
||||
final VideoStore videoStore;
|
||||
|
||||
const VideoOverlay({Key? key, required this.videoStore}) : super(key: key);
|
||||
|
||||
Future<void> _showSleepTimerDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Sleep Timer'),
|
||||
content: Observer(
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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'),
|
||||
],
|
||||
),
|
||||
if (videoStore.sleepTimer != null && videoStore.sleepTimer!.isActive)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.timer),
|
||||
Text(' ${videoStore.timeRemaining.toString().split('.')[0]}'),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'Cancel Timer',
|
||||
onPressed: videoStore.cancelSleepTimer,
|
||||
icon: const Icon(Icons.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
child: const Text('Dismiss'),
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: videoStore.sleepHours == 0 && videoStore.sleepMinutes == 0
|
||||
? null
|
||||
: () => videoStore.updateSleepTimer(
|
||||
onTimerFinished: () => navigatorKey.currentState?.popUntil((route) => route.isFirst),
|
||||
),
|
||||
child: const Text('Set Timer'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
|
||||
final backButton = Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
);
|
||||
|
||||
final settingsButton = IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: const Icon(
|
||||
Icons.settings,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Settings(settingsStore: videoStore.settingsStore),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final chatOverlayButton = IconButton(
|
||||
tooltip: 'Toggle Chat Overlay',
|
||||
onPressed: () => videoStore.settingsStore.fullScreenChatOverlay = !videoStore.settingsStore.fullScreenChatOverlay,
|
||||
icon: const Icon(Icons.chat_bubble_outline),
|
||||
color: Colors.white,
|
||||
);
|
||||
|
||||
final refreshButton = IconButton(
|
||||
tooltip: 'Refresh',
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.handleRefresh,
|
||||
);
|
||||
|
||||
final fullScreenButton = IconButton(
|
||||
tooltip: videoStore.settingsStore.fullScreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
|
||||
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 sleepTimerButton = IconButton(
|
||||
tooltip: 'Sleep Timer',
|
||||
icon: const Icon(
|
||||
Icons.timer,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => _showSleepTimerDialog(context),
|
||||
);
|
||||
|
||||
final streamInfo = videoStore.streamInfo;
|
||||
if (streamInfo == null) {
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
backButton,
|
||||
const Spacer(),
|
||||
settingsButton,
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (videoStore.settingsStore.fullScreen && orientation == Orientation.landscape) chatOverlayButton,
|
||||
refreshButton,
|
||||
if (orientation == Orientation.landscape) fullScreenButton,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName) ? streamInfo.userName : '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
final streamer = Row(
|
||||
children: [
|
||||
ProfilePicture(
|
||||
userLogin: streamInfo.userLogin,
|
||||
radius: 10.0,
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
message: streamerName,
|
||||
preferBelow: false,
|
||||
child: Text(
|
||||
streamerName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
backButton,
|
||||
const Spacer(),
|
||||
sleepTimerButton,
|
||||
settingsButton,
|
||||
],
|
||||
),
|
||||
|
||||
// Add a play button when paused for Android
|
||||
// When an ad is paused on Android there is no way to unpause, so a play button is necessary.
|
||||
if (Platform.isAndroid)
|
||||
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,
|
||||
),
|
||||
)
|
||||
else if (!videoStore.paused)
|
||||
Center(
|
||||
child: IconButton(
|
||||
tooltip: 'Pause',
|
||||
iconSize: 50.0,
|
||||
icon: const Icon(
|
||||
Icons.pause,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.handlePausePlay,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: videoStore.handleExpand,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: videoStore.settingsStore.expandInfo
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
streamer,
|
||||
const SizedBox(height: 5.0),
|
||||
Tooltip(
|
||||
message: videoStore.streamInfo!.title.trim(),
|
||||
preferBelow: false,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Text(
|
||||
videoStore.streamInfo!.title.trim(),
|
||||
maxLines: orientation == Orientation.portrait ? 1 : 5,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
'${videoStore.streamInfo?.gameName} \u2022 ${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: streamer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (Platform.isIOS && videoStore.settingsStore.pictureInPicture)
|
||||
IconButton(
|
||||
tooltip: 'Picture-in-Picture',
|
||||
icon: const Icon(
|
||||
Icons.picture_in_picture_alt_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.requestPictureInPicture,
|
||||
),
|
||||
if (videoStore.settingsStore.fullScreen && orientation == Orientation.landscape) chatOverlayButton,
|
||||
refreshButton,
|
||||
if (orientation == Orientation.landscape) fullScreenButton
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
|
@ -7,17 +10,20 @@ import 'package:frosty/api/seventv_api.dart';
|
|||
import 'package:frosty/api/twitch_api.dart';
|
||||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/core/auth/auth_store.dart';
|
||||
import 'package:frosty/main.dart';
|
||||
import 'package:frosty/screens/channel/chat/chat.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_details_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/channel/stores/video_store.dart';
|
||||
import 'package:frosty/screens/channel/video/video.dart';
|
||||
import 'package:frosty/screens/channel/video/video_overlay.dart';
|
||||
import 'package:frosty/screens/settings/settings.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/profile_picture.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
/// Creates a widget that shows the video stream (if live) and chat of the given user.
|
||||
class VideoChat extends StatefulWidget {
|
||||
final String userId;
|
||||
final String userName;
|
||||
|
|
@ -66,14 +72,17 @@ class _VideoChatState extends State<VideoChat> {
|
|||
Widget build(BuildContext context) {
|
||||
final settingsStore = _chatStore.settings;
|
||||
|
||||
final player = Video(
|
||||
key: _videoKey,
|
||||
videoStore: _videoStore,
|
||||
final player = GestureDetector(
|
||||
onLongPress: _videoStore.handleToggleOverlay,
|
||||
child: _Video(
|
||||
key: _videoKey,
|
||||
videoStore: _videoStore,
|
||||
),
|
||||
);
|
||||
|
||||
final overlay = VideoOverlay(videoStore: _videoStore);
|
||||
final videoOverlay = _VideoOverlay(videoStore: _videoStore);
|
||||
|
||||
final video = GestureDetector(
|
||||
final overlay = GestureDetector(
|
||||
onLongPress: _videoStore.handleToggleOverlay,
|
||||
onTap: () {
|
||||
if (_chatStore.assetsStore.showEmoteMenu) {
|
||||
|
|
@ -87,33 +96,37 @@ class _VideoChatState extends State<VideoChat> {
|
|||
}
|
||||
},
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (_videoStore.settingsStore.showOverlay) {
|
||||
return Stack(
|
||||
children: [
|
||||
player,
|
||||
if (_videoStore.paused || _videoStore.streamInfo == null)
|
||||
overlay
|
||||
else
|
||||
AnimatedOpacity(
|
||||
opacity: _videoStore.overlayVisible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: ColoredBox(
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.5),
|
||||
child: IgnorePointer(
|
||||
ignoring: !_videoStore.overlayVisible,
|
||||
child: overlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return player;
|
||||
builder: (_) {
|
||||
if (_videoStore.paused || _videoStore.streamInfo == null) return videoOverlay;
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: _videoStore.overlayVisible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: ColoredBox(
|
||||
color: Colors.black.withOpacity(settingsStore.overlayOpacity),
|
||||
child: IgnorePointer(
|
||||
ignoring: !_videoStore.overlayVisible,
|
||||
child: videoOverlay,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final video = Observer(
|
||||
builder: (context) {
|
||||
if (!_videoStore.settingsStore.showOverlay) return player;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
player,
|
||||
overlay,
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final chat = Chat(
|
||||
key: _chatKey,
|
||||
chatStore: _chatStore,
|
||||
|
|
@ -157,12 +170,26 @@ class _VideoChatState extends State<VideoChat> {
|
|||
duration: const Duration(milliseconds: 200),
|
||||
width: _chatStore.expandChat
|
||||
? MediaQuery.of(context).size.width / 2
|
||||
: MediaQuery.of(context).size.width * _chatStore.settings.landscapeChatWidth,
|
||||
: MediaQuery.of(context).size.width * _chatStore.settings.chatWidth,
|
||||
curve: Curves.ease,
|
||||
color: _chatStore.settings.fullScreen ? null : Theme.of(context).scaffoldBackgroundColor,
|
||||
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(
|
||||
|
|
@ -170,38 +197,23 @@ class _VideoChatState extends State<VideoChat> {
|
|||
child: settingsStore.showVideo
|
||||
? settingsStore.fullScreen
|
||||
? Stack(
|
||||
alignment: settingsStore.landscapeChatLeftSide ? Alignment.topLeft : Alignment.topRight,
|
||||
children: [
|
||||
video,
|
||||
IgnorePointer(
|
||||
ignoring: _videoStore.overlayVisible,
|
||||
child: Visibility(
|
||||
visible: settingsStore.fullScreenChatOverlay,
|
||||
maintainState: true,
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(color: Colors.white),
|
||||
child: landscapeChat,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
],
|
||||
? [landscapeChat, Expanded(child: video)]
|
||||
: [Expanded(child: video), landscapeChat],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
appBar,
|
||||
Expanded(child: chat),
|
||||
],
|
||||
children: [appBar, Expanded(child: chat)],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -218,13 +230,9 @@ class _VideoChatState extends State<VideoChat> {
|
|||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (settingsStore.showVideo) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: video,
|
||||
);
|
||||
}
|
||||
return appBar;
|
||||
if (!settingsStore.showVideo) return appBar;
|
||||
|
||||
return AspectRatio(aspectRatio: 16 / 9, child: video);
|
||||
},
|
||||
),
|
||||
Expanded(child: chat),
|
||||
|
|
@ -247,6 +255,376 @@ class _VideoChatState extends State<VideoChat> {
|
|||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
|
||||
SystemChrome.setPreferredOrientations([]);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [WebView] widget that shows a channel's video stream.
|
||||
class _Video extends StatelessWidget {
|
||||
final VideoStore videoStore;
|
||||
|
||||
const _Video({
|
||||
Key? key,
|
||||
required this.videoStore,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebView(
|
||||
backgroundColor: Colors.black,
|
||||
initialUrl: videoStore.videoUrl,
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
allowsInlineMediaPlayback: true,
|
||||
initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
|
||||
onWebViewCreated: (controller) => videoStore.controller = controller,
|
||||
onPageFinished: (string) => videoStore.initVideo(),
|
||||
navigationDelegate: videoStore.handleNavigation,
|
||||
javascriptChannels: videoStore.javascriptChannels,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a widget containing controls which enable interactions with an underlying [_Video] widget.
|
||||
class _VideoOverlay extends StatelessWidget {
|
||||
final VideoStore videoStore;
|
||||
|
||||
const _VideoOverlay({Key? key, required this.videoStore}) : super(key: key);
|
||||
|
||||
Future<void> _showSleepTimerDialog(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Sleep Timer'),
|
||||
content: Observer(
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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'),
|
||||
],
|
||||
),
|
||||
if (videoStore.sleepTimer != null && videoStore.sleepTimer!.isActive)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.timer),
|
||||
Text(' ${videoStore.timeRemaining.toString().split('.')[0]}'),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'Cancel Timer',
|
||||
onPressed: videoStore.cancelSleepTimer,
|
||||
icon: const Icon(Icons.cancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
child: const Text('Dismiss'),
|
||||
),
|
||||
Observer(
|
||||
builder: (context) => ElevatedButton(
|
||||
onPressed: videoStore.sleepHours == 0 && videoStore.sleepMinutes == 0
|
||||
? null
|
||||
: () => videoStore.updateSleepTimer(
|
||||
onTimerFinished: () => navigatorKey.currentState?.popUntil((route) => route.isFirst),
|
||||
),
|
||||
child: const Text('Set Timer'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final orientation = MediaQuery.of(context).orientation;
|
||||
|
||||
final backButton = Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: IconButton(
|
||||
tooltip: 'Back',
|
||||
icon: Icon(
|
||||
Icons.adaptive.arrow_back,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
);
|
||||
|
||||
final settingsButton = IconButton(
|
||||
tooltip: 'Settings',
|
||||
icon: const Icon(
|
||||
Icons.settings,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
child: Settings(settingsStore: videoStore.settingsStore),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
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),
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
|
||||
final refreshButton = IconButton(
|
||||
tooltip: 'Refresh',
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.handleRefresh,
|
||||
);
|
||||
|
||||
final fullScreenButton = IconButton(
|
||||
tooltip: videoStore.settingsStore.fullScreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
|
||||
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 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([]);
|
||||
} else {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeRight,
|
||||
DeviceOrientation.landscapeLeft,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
SystemChrome.setPreferredOrientations([]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final streamInfo = videoStore.streamInfo;
|
||||
if (streamInfo == null) {
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
backButton,
|
||||
const Spacer(),
|
||||
if (videoStore.settingsStore.fullScreen && orientation == Orientation.landscape) chatOverlayButton,
|
||||
settingsButton,
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
refreshButton,
|
||||
if (!videoStore.isIPad) rotateButton,
|
||||
if (orientation == Orientation.landscape) fullScreenButton,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final streamerName = regexEnglish.hasMatch(streamInfo.userName) ? streamInfo.userName : '${streamInfo.userName} (${streamInfo.userLogin})';
|
||||
final streamer = Row(
|
||||
children: [
|
||||
ProfilePicture(
|
||||
userLogin: streamInfo.userLogin,
|
||||
radius: 10.0,
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
Flexible(
|
||||
child: Tooltip(
|
||||
message: streamerName,
|
||||
preferBelow: false,
|
||||
child: Text(
|
||||
streamerName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
backButton,
|
||||
const Spacer(),
|
||||
if (videoStore.settingsStore.fullScreen && orientation == Orientation.landscape) chatOverlayButton,
|
||||
sleepTimerButton,
|
||||
settingsButton,
|
||||
],
|
||||
),
|
||||
|
||||
// Add a play button when paused for Android
|
||||
// When an ad is paused on Android there is no way to unpause, so a play button is necessary.
|
||||
if (Platform.isAndroid)
|
||||
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,
|
||||
),
|
||||
)
|
||||
else if (!videoStore.paused)
|
||||
Center(
|
||||
child: IconButton(
|
||||
tooltip: 'Pause',
|
||||
iconSize: 50.0,
|
||||
icon: const Icon(
|
||||
Icons.pause,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.handlePausePlay,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: videoStore.handleExpand,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: videoStore.settingsStore.expandInfo
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
streamer,
|
||||
const SizedBox(height: 5.0),
|
||||
Tooltip(
|
||||
message: videoStore.streamInfo!.title.trim(),
|
||||
preferBelow: false,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Text(
|
||||
videoStore.streamInfo!.title.trim(),
|
||||
maxLines: orientation == Orientation.portrait ? 1 : 5,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
Text(
|
||||
'${videoStore.streamInfo?.gameName} \u2022 ${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: streamer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (Platform.isIOS && videoStore.settingsStore.pictureInPicture)
|
||||
IconButton(
|
||||
tooltip: 'Picture-in-Picture',
|
||||
icon: const Icon(
|
||||
Icons.picture_in_picture_alt_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: videoStore.requestPictureInPicture,
|
||||
),
|
||||
refreshButton,
|
||||
if (!videoStore.isIPad) rotateButton,
|
||||
if (orientation == Orientation.landscape) fullScreenButton,
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
),
|
||||
),
|
||||
children: [
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Autocomplete'),
|
||||
subtitle: const Text('Shows a bar that suggests matching emotes and mentions when typing.'),
|
||||
value: settingsStore.autocomplete,
|
||||
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.autocomplete = newValue : null,
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
|
|
@ -75,15 +83,32 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
children: [
|
||||
const Text('Landscape Chat Width'),
|
||||
const Spacer(),
|
||||
Text('${(settingsStore.landscapeChatWidth * 100).toInt()}%'),
|
||||
Text('${(settingsStore.chatWidth * 100).toStringAsFixed(0)}%'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.landscapeChatWidth,
|
||||
value: settingsStore.chatWidth,
|
||||
min: 0.2,
|
||||
max: 0.8,
|
||||
divisions: 12,
|
||||
onChanged: (newValue) => settingsStore.landscapeChatWidth = newValue,
|
||||
max: 0.6,
|
||||
divisions: 8,
|
||||
onChanged: (newValue) => settingsStore.chatWidth = newValue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Landscape Chat Overlay Opacity'),
|
||||
const Spacer(),
|
||||
Text('${(settingsStore.fullScreenChatOverlayOpacity * 100).toStringAsFixed(0)}%'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.fullScreenChatOverlayOpacity,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
divisions: 10,
|
||||
onChanged: (newValue) => settingsStore.fullScreenChatOverlayOpacity = newValue,
|
||||
),
|
||||
),
|
||||
const SectionHeader(
|
||||
|
|
@ -91,13 +116,6 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
fontSize: 12.0,
|
||||
padding: sectionPadding,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Emote Autocomplete'),
|
||||
subtitle: const Text('Shows a bar that suggests matching emotes when typing.'),
|
||||
value: settingsStore.emoteAutocomplete,
|
||||
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.emoteAutocomplete = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Zero-Width Emotes'),
|
||||
|
|
|
|||
|
|
@ -49,6 +49,23 @@ class VideoSettings extends StatelessWidget {
|
|||
value: settingsStore.toggleableOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.toggleableOverlay = newValue : null,
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('Custom Overlay Opacity'),
|
||||
const Spacer(),
|
||||
Text('${(settingsStore.overlayOpacity * 100).toStringAsFixed(0)}%'),
|
||||
],
|
||||
),
|
||||
subtitle: Slider.adaptive(
|
||||
value: settingsStore.overlayOpacity,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
divisions: 10,
|
||||
onChanged: (newValue) => settingsStore.overlayOpacity = newValue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
static const defaultPictureInPicture = false;
|
||||
|
||||
static const defaultOverlayOpacity = 0.5;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowVideo)
|
||||
@observable
|
||||
var showVideo = defaultShowVideo;
|
||||
|
|
@ -62,6 +64,10 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var pictureInPicture = defaultPictureInPicture;
|
||||
|
||||
@JsonKey(defaultValue: defaultOverlayOpacity)
|
||||
@observable
|
||||
var overlayOpacity = defaultOverlayOpacity;
|
||||
|
||||
// * Chat Settings
|
||||
static const defaultShowBottomBar = true;
|
||||
|
||||
|
|
@ -85,14 +91,16 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
static const defaultEmoteScale = 1.0;
|
||||
|
||||
static const defaultEmoteAutocomplete = true;
|
||||
static const defaultAutocomplete = true;
|
||||
|
||||
static const defaultLandscapeChatWidth = 0.3;
|
||||
static const defaultChatWidth = 0.3;
|
||||
|
||||
static const defaultLandscapeChatLeftSide = false;
|
||||
|
||||
static const defaultChatDelay = 0.0;
|
||||
|
||||
static const defaultFullScreenChatOverlayOpacity = 0.5;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowBottomBar)
|
||||
@observable
|
||||
var showBottomBar = defaultShowBottomBar;
|
||||
|
|
@ -137,18 +145,22 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var emoteScale = defaultEmoteScale;
|
||||
|
||||
@JsonKey(defaultValue: defaultEmoteAutocomplete)
|
||||
@JsonKey(defaultValue: defaultAutocomplete)
|
||||
@observable
|
||||
var emoteAutocomplete = defaultEmoteAutocomplete;
|
||||
var autocomplete = defaultAutocomplete;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeChatWidth)
|
||||
@JsonKey(defaultValue: defaultChatWidth)
|
||||
@observable
|
||||
var landscapeChatWidth = defaultLandscapeChatWidth;
|
||||
var chatWidth = defaultChatWidth;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeChatLeftSide)
|
||||
@observable
|
||||
var landscapeChatLeftSide = defaultLandscapeChatLeftSide;
|
||||
|
||||
@JsonKey(defaultValue: defaultFullScreenChatOverlayOpacity)
|
||||
@observable
|
||||
var fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
|
||||
|
||||
// * Other settings
|
||||
static const defaultSendCrashLogs = true;
|
||||
|
||||
|
|
@ -192,6 +204,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
showOverlay = defaultShowOverlay;
|
||||
toggleableOverlay = defaultToggleableOverlay;
|
||||
pictureInPicture = defaultPictureInPicture;
|
||||
overlayOpacity = defaultOverlayOpacity;
|
||||
|
||||
// * Chat Settings
|
||||
showBottomBar = defaultShowBottomBar;
|
||||
|
|
@ -205,10 +218,11 @@ abstract class _SettingsStoreBase with Store {
|
|||
messageSpacing = defaultMessageSpacing;
|
||||
badgeScale = defaultBadgeScale;
|
||||
emoteScale = defaultEmoteScale;
|
||||
emoteAutocomplete = defaultEmoteAutocomplete;
|
||||
landscapeChatWidth = defaultLandscapeChatWidth;
|
||||
autocomplete = defaultAutocomplete;
|
||||
chatWidth = defaultChatWidth;
|
||||
landscapeChatLeftSide = false;
|
||||
chatDelay = defaultChatDelay;
|
||||
fullScreenChatOverlayOpacity = defaultFullScreenChatOverlayOpacity;
|
||||
|
||||
// * Other settings
|
||||
sendCrashLogs = defaultSendCrashLogs;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..showOverlay = json['showOverlay'] as bool? ?? true
|
||||
..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false
|
||||
..pictureInPicture = json['pictureInPicture'] as bool? ?? false
|
||||
..overlayOpacity = (json['overlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
..showBottomBar = json['showBottomBar'] as bool? ?? true
|
||||
..showDeletedMessages = json['showDeletedMessages'] as bool? ?? false
|
||||
..showZeroWidth = json['showZeroWidth'] as bool? ?? false
|
||||
|
|
@ -33,10 +34,11 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..messageScale = (json['messageScale'] as num?)?.toDouble() ?? 1.0
|
||||
..badgeScale = (json['badgeScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteScale = (json['emoteScale'] as num?)?.toDouble() ?? 1.0
|
||||
..emoteAutocomplete = json['emoteAutocomplete'] as bool? ?? true
|
||||
..landscapeChatWidth =
|
||||
(json['landscapeChatWidth'] as num?)?.toDouble() ?? 0.3
|
||||
..autocomplete = json['autocomplete'] as bool? ?? true
|
||||
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.3
|
||||
..landscapeChatLeftSide = json['landscapeChatLeftSide'] as bool? ?? false
|
||||
..fullScreenChatOverlayOpacity =
|
||||
(json['fullScreenChatOverlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
..sendCrashLogs = json['sendCrashLogs'] as bool? ?? true
|
||||
..fullScreen = json['fullScreen'] as bool? ?? false
|
||||
..expandInfo = json['expandInfo'] as bool? ?? true
|
||||
|
|
@ -53,6 +55,7 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'showOverlay': instance.showOverlay,
|
||||
'toggleableOverlay': instance.toggleableOverlay,
|
||||
'pictureInPicture': instance.pictureInPicture,
|
||||
'overlayOpacity': instance.overlayOpacity,
|
||||
'showBottomBar': instance.showBottomBar,
|
||||
'showDeletedMessages': instance.showDeletedMessages,
|
||||
'showZeroWidth': instance.showZeroWidth,
|
||||
|
|
@ -64,9 +67,10 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'messageScale': instance.messageScale,
|
||||
'badgeScale': instance.badgeScale,
|
||||
'emoteScale': instance.emoteScale,
|
||||
'emoteAutocomplete': instance.emoteAutocomplete,
|
||||
'landscapeChatWidth': instance.landscapeChatWidth,
|
||||
'autocomplete': instance.autocomplete,
|
||||
'chatWidth': instance.chatWidth,
|
||||
'landscapeChatLeftSide': instance.landscapeChatLeftSide,
|
||||
'fullScreenChatOverlayOpacity': instance.fullScreenChatOverlayOpacity,
|
||||
'sendCrashLogs': instance.sendCrashLogs,
|
||||
'fullScreen': instance.fullScreen,
|
||||
'expandInfo': instance.expandInfo,
|
||||
|
|
@ -222,6 +226,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$overlayOpacityAtom =
|
||||
Atom(name: '_SettingsStoreBase.overlayOpacity', context: context);
|
||||
|
||||
@override
|
||||
double get overlayOpacity {
|
||||
_$overlayOpacityAtom.reportRead();
|
||||
return super.overlayOpacity;
|
||||
}
|
||||
|
||||
@override
|
||||
set overlayOpacity(double value) {
|
||||
_$overlayOpacityAtom.reportWrite(value, super.overlayOpacity, () {
|
||||
super.overlayOpacity = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showBottomBarAtom =
|
||||
Atom(name: '_SettingsStoreBase.showBottomBar', context: context);
|
||||
|
||||
|
|
@ -399,35 +419,35 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$emoteAutocompleteAtom =
|
||||
Atom(name: '_SettingsStoreBase.emoteAutocomplete', context: context);
|
||||
late final _$autocompleteAtom =
|
||||
Atom(name: '_SettingsStoreBase.autocomplete', context: context);
|
||||
|
||||
@override
|
||||
bool get emoteAutocomplete {
|
||||
_$emoteAutocompleteAtom.reportRead();
|
||||
return super.emoteAutocomplete;
|
||||
bool get autocomplete {
|
||||
_$autocompleteAtom.reportRead();
|
||||
return super.autocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
set emoteAutocomplete(bool value) {
|
||||
_$emoteAutocompleteAtom.reportWrite(value, super.emoteAutocomplete, () {
|
||||
super.emoteAutocomplete = value;
|
||||
set autocomplete(bool value) {
|
||||
_$autocompleteAtom.reportWrite(value, super.autocomplete, () {
|
||||
super.autocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$landscapeChatWidthAtom =
|
||||
Atom(name: '_SettingsStoreBase.landscapeChatWidth', context: context);
|
||||
late final _$chatWidthAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatWidth', context: context);
|
||||
|
||||
@override
|
||||
double get landscapeChatWidth {
|
||||
_$landscapeChatWidthAtom.reportRead();
|
||||
return super.landscapeChatWidth;
|
||||
double get chatWidth {
|
||||
_$chatWidthAtom.reportRead();
|
||||
return super.chatWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeChatWidth(double value) {
|
||||
_$landscapeChatWidthAtom.reportWrite(value, super.landscapeChatWidth, () {
|
||||
super.landscapeChatWidth = value;
|
||||
set chatWidth(double value) {
|
||||
_$chatWidthAtom.reportWrite(value, super.chatWidth, () {
|
||||
super.chatWidth = value;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -448,6 +468,24 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
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 _$sendCrashLogsAtom =
|
||||
Atom(name: '_SettingsStoreBase.sendCrashLogs', context: context);
|
||||
|
||||
|
|
@ -554,6 +592,7 @@ showVideo: ${showVideo},
|
|||
showOverlay: ${showOverlay},
|
||||
toggleableOverlay: ${toggleableOverlay},
|
||||
pictureInPicture: ${pictureInPicture},
|
||||
overlayOpacity: ${overlayOpacity},
|
||||
showBottomBar: ${showBottomBar},
|
||||
showDeletedMessages: ${showDeletedMessages},
|
||||
showZeroWidth: ${showZeroWidth},
|
||||
|
|
@ -565,9 +604,10 @@ messageSpacing: ${messageSpacing},
|
|||
messageScale: ${messageScale},
|
||||
badgeScale: ${badgeScale},
|
||||
emoteScale: ${emoteScale},
|
||||
emoteAutocomplete: ${emoteAutocomplete},
|
||||
landscapeChatWidth: ${landscapeChatWidth},
|
||||
autocomplete: ${autocomplete},
|
||||
chatWidth: ${chatWidth},
|
||||
landscapeChatLeftSide: ${landscapeChatLeftSide},
|
||||
fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity},
|
||||
sendCrashLogs: ${sendCrashLogs},
|
||||
fullScreen: ${fullScreen},
|
||||
expandInfo: ${expandInfo},
|
||||
|
|
|
|||
66
pubspec.lock
66
pubspec.lock
|
|
@ -168,7 +168,7 @@ packages:
|
|||
name: convert
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.0.2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -182,7 +182,7 @@ packages:
|
|||
name: cupertino_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -190,6 +190,48 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
device_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.4"
|
||||
device_info_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
device_info_plus_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0+1"
|
||||
device_info_plus_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
device_info_plus_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -243,7 +285,7 @@ packages:
|
|||
name: flutter_launcher_icons
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.2"
|
||||
version: "0.9.3"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -264,7 +306,7 @@ packages:
|
|||
name: flutter_native_splash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.3"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -575,7 +617,7 @@ packages:
|
|||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.10"
|
||||
version: "2.0.11"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -596,7 +638,7 @@ packages:
|
|||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
version: "2.1.7"
|
||||
path_provider_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -617,7 +659,7 @@ packages:
|
|||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.0.7"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -694,7 +736,7 @@ packages:
|
|||
name: rxdart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.27.3"
|
||||
version: "0.27.4"
|
||||
sentry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -902,7 +944,7 @@ packages:
|
|||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
version: "6.1.3"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -993,21 +1035,21 @@ packages:
|
|||
name: webview_flutter_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.9"
|
||||
version: "2.8.11"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.0"
|
||||
version: "2.8.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.4.1+23
|
||||
version: 1.5.0+24
|
||||
|
||||
environment:
|
||||
sdk: ">=2.15.0 <3.0.0"
|
||||
|
|
@ -54,6 +54,7 @@ dependencies:
|
|||
package_info_plus: ^1.3.0
|
||||
collection: ^1.16.0
|
||||
flutter_cache_manager: ^3.3.0
|
||||
device_info_plus: ^3.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue