diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4d74199f..f93771fc 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -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
diff --git a/android/app/src/main/res/drawable-night-v21/background.png b/android/app/src/main/res/drawable-night-v21/background.png
deleted file mode 100644
index a8ba2bab..00000000
Binary files a/android/app/src/main/res/drawable-night-v21/background.png and /dev/null differ
diff --git a/android/app/src/main/res/drawable-night/background.png b/android/app/src/main/res/drawable-night/background.png
deleted file mode 100644
index a8ba2bab..00000000
Binary files a/android/app/src/main/res/drawable-night/background.png and /dev/null differ
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2de1b00c..b90e2d34 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -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
diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
deleted file mode 100644
index a8ba2bab..00000000
Binary files a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
index f3387d4a..00cabce8 100644
--- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -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" : {
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png
deleted file mode 100644
index 65e45c48..00000000
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png
deleted file mode 100644
index b45507c1..00000000
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png
deleted file mode 100644
index d56cc8fd..00000000
Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png and /dev/null differ
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
index 15efa880..0740e086 100644
--- a/ios/Runner/Base.lproj/LaunchScreen.storyboard
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -18,7 +18,6 @@
-
@@ -41,6 +40,5 @@
-
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index 6abd887e..084a1491 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -75,86 +75,86 @@ void main() async {
// Navigator key for sleep timer. Allows navigation popping without context.
final navigatorKey = GlobalKey();
+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();
diff --git a/lib/screens/channel/chat/chat.dart b/lib/screens/channel/chat/chat.dart
index b1145686..b8f7765e 100644
--- a/lib/screens/channel/chat/chat.dart
+++ b/lib/screens/channel/chat/chat.dart
@@ -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,
diff --git a/lib/screens/channel/chat/details/chat_users_list.dart b/lib/screens/channel/chat/details/chat_users_list.dart
index 6c652881..13c85fda 100644
--- a/lib/screens/channel/chat/details/chat_users_list.dart
+++ b/lib/screens/channel/chat/details/chat_users_list.dart
@@ -43,6 +43,8 @@ class _ChattersListState extends State {
widget.chatDetails.showJumpButton = true;
}
});
+
+ widget.chatDetails.updateChatters(widget.userLogin);
}
@override
@@ -58,7 +60,6 @@ class _ChattersListState extends State {
];
final chatDetailsStore = widget.chatDetails;
- chatDetailsStore.updateChatters(widget.userLogin);
return Column(
children: [
@@ -116,6 +117,7 @@ class _ChattersListState extends State {
}
return CustomScrollView(
+ physics: const AlwaysScrollableScrollPhysics(),
controller: _scrollController,
slivers: [
SliverPadding(
diff --git a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart
index 7b8235b1..c942727f 100644
--- a/lib/screens/channel/chat/widgets/chat_bottom_bar.dart
+++ b/lib/screens/channel/chat/widgets/chat_bottom_bar.dart
@@ -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),
diff --git a/lib/screens/channel/stores/chat_details_store.dart b/lib/screens/channel/stores/chat_details_store.dart
index 6178f9d4..a89f277d 100644
--- a/lib/screens/channel/stores/chat_details_store.dart
+++ b/lib/screens/channel/stores/chat_details_store.dart
@@ -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> 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 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;
diff --git a/lib/screens/channel/stores/chat_details_store.g.dart b/lib/screens/channel/stores/chat_details_store.g.dart
index e443227d..dd6fa147 100644
--- a/lib/screens/channel/stores/chat_details_store.g.dart
+++ b/lib/screens/channel/stores/chat_details_store.g.dart
@@ -16,6 +16,13 @@ mixin _$ChatDetailsStore on ChatDetailsStoreBase, Store {
Computed>>(() => super.filteredUsers,
name: 'ChatDetailsStoreBase.filteredUsers'))
.value;
+ Computed>? _$allChattersComputed;
+
+ @override
+ List get allChatters =>
+ (_$allChattersComputed ??= Computed>(() => 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}
''';
}
}
diff --git a/lib/screens/channel/stores/chat_store.dart b/lib/screens/channel/stores/chat_store.dart
index 1d822d9e..1a57128f 100644
--- a/lib/screens/channel/stores/chat_store.dart
+++ b/lib/screens/channel/stores/chat_store.dart
@@ -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} ');
diff --git a/lib/screens/channel/stores/chat_store.g.dart b/lib/screens/channel/stores/chat_store.g.dart
index f363ce6d..4d4189c8 100644
--- a/lib/screens/channel/stores/chat_store.g.dart
+++ b/lib/screens/channel/stores/chat_store.g.dart
@@ -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;
});
}
diff --git a/lib/screens/channel/stores/video_store.dart b/lib/screens/channel/stores/video_store.dart
index 44e2f2c2..c0e20e90 100644
--- a/lib/screens/channel/stores/video_store.dart
+++ b/lib/screens/channel/stores/video_store.dart
@@ -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 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() {
diff --git a/lib/screens/channel/stores/video_store.g.dart b/lib/screens/channel/stores/video_store.g.dart
index 28710c74..914038ce 100644
--- a/lib/screens/channel/stores/video_store.g.dart
+++ b/lib/screens/channel/stores/video_store.g.dart
@@ -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 initVideo() {
+ return _$initVideoAsyncAction.run(() => super.initVideo());
+ }
+
late final _$VideoStoreBaseActionController =
ActionController(name: 'VideoStoreBase', context: context);
diff --git a/lib/screens/channel/video/video.dart b/lib/screens/channel/video/video.dart
deleted file mode 100644
index c52cdba5..00000000
--- a/lib/screens/channel/video/video.dart
+++ /dev/null
@@ -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,
- );
- }
-}
diff --git a/lib/screens/channel/video/video_overlay.dart b/lib/screens/channel/video/video_overlay.dart
deleted file mode 100644
index d8437453..00000000
--- a/lib/screens/channel/video/video_overlay.dart
+++ /dev/null
@@ -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 _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
- ],
- ),
- )
- ],
- );
- },
- );
- }
-}
diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart
index 2bc08259..daeff269 100644
--- a/lib/screens/channel/video_chat.dart
+++ b/lib/screens/channel/video_chat.dart
@@ -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 {
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 {
}
},
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 {
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 {
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 {
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 {
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 _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,
+ ],
+ ),
+ )
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/lib/screens/settings/sections/chat_settings.dart b/lib/screens/settings/sections/chat_settings.dart
index 59585a76..515a4449 100644
--- a/lib/screens/settings/sections/chat_settings.dart
+++ b/lib/screens/settings/sections/chat_settings.dart
@@ -38,6 +38,14 @@ class _ChatSettingsState extends State {
),
),
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 {
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 {
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'),
diff --git a/lib/screens/settings/sections/video_settings.dart b/lib/screens/settings/sections/video_settings.dart
index a2280088..9b5f2631 100644
--- a/lib/screens/settings/sections/video_settings.dart
+++ b/lib/screens/settings/sections/video_settings.dart
@@ -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,
+ ),
+ ),
],
),
);
diff --git a/lib/screens/settings/stores/settings_store.dart b/lib/screens/settings/stores/settings_store.dart
index 1107345c..69f61d4b 100644
--- a/lib/screens/settings/stores/settings_store.dart
+++ b/lib/screens/settings/stores/settings_store.dart
@@ -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;
diff --git a/lib/screens/settings/stores/settings_store.g.dart b/lib/screens/settings/stores/settings_store.g.dart
index ce489e1c..7acdbab9 100644
--- a/lib/screens/settings/stores/settings_store.g.dart
+++ b/lib/screens/settings/stores/settings_store.g.dart
@@ -18,6 +18,7 @@ SettingsStore _$SettingsStoreFromJson(Map 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 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 _$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 _$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},
diff --git a/pubspec.lock b/pubspec.lock
index 0f2746d1..af82478a 100644
--- a/pubspec.lock
+++ b/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:
diff --git a/pubspec.yaml b/pubspec.yaml
index 0f0fbd06..6fa13481 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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: