commit
c430644204
28 changed files with 888 additions and 469 deletions
|
|
@ -19,6 +19,7 @@
|
|||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ PODS:
|
|||
- Flutter
|
||||
- path_provider_ios (0.0.1):
|
||||
- Flutter
|
||||
- Sentry (7.19.0):
|
||||
- Sentry/Core (= 7.19.0)
|
||||
- Sentry/Core (7.19.0)
|
||||
- Sentry (7.22.0):
|
||||
- Sentry/Core (= 7.22.0)
|
||||
- Sentry/Core (7.22.0)
|
||||
- sentry_flutter (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry (~> 7.19.0)
|
||||
- Sentry (~> 7.22.0)
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.2):
|
||||
|
|
@ -27,6 +27,8 @@ PODS:
|
|||
- FMDB (>= 2.7.5)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
|
||||
|
|
@ -41,6 +43,7 @@ DEPENDENCIES:
|
|||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
|
|
@ -69,6 +72,8 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock:
|
||||
:path: ".symlinks/plugins/wakelock/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
|
|
@ -80,11 +85,12 @@ SPEC CHECKSUMS:
|
|||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
Sentry: d6b16e66a0ad0c39a0b76d9a1194e68e78c37ce6
|
||||
sentry_flutter: 609b096af497fa64af7c6c94e792d0780aa472a6
|
||||
Sentry: 30b51086ca9aac23337880152e95538f7e177f7f
|
||||
sentry_flutter: eaca55af5c951f4be6c4b1daddce1744c37d8476
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
|
||||
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
|
||||
|
|
|
|||
|
|
@ -233,7 +233,6 @@ class Emote7TV {
|
|||
final List<String> visibilitySimple;
|
||||
final String mime;
|
||||
final int status;
|
||||
final List<String> tags;
|
||||
final List<int> width;
|
||||
final List<int> height;
|
||||
final List<List<String>> urls;
|
||||
|
|
@ -246,7 +245,6 @@ class Emote7TV {
|
|||
this.visibilitySimple,
|
||||
this.mime,
|
||||
this.status,
|
||||
this.tags,
|
||||
this.width,
|
||||
this.height,
|
||||
this.urls,
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ Emote7TV _$Emote7TVFromJson(Map<String, dynamic> json) => Emote7TV(
|
|||
.toList(),
|
||||
json['mime'] as String,
|
||||
json['status'] as int,
|
||||
(json['tags'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
(json['width'] as List<dynamic>).map((e) => e as int).toList(),
|
||||
(json['height'] as List<dynamic>).map((e) => e as int).toList(),
|
||||
(json['urls'] as List<dynamic>)
|
||||
|
|
@ -140,7 +139,7 @@ Map<String, dynamic> _$EmoteToJson(Emote instance) => <String, dynamic>{
|
|||
'height': instance.height,
|
||||
'zeroWidth': instance.zeroWidth,
|
||||
'url': instance.url,
|
||||
'type': _$EmoteTypeEnumMap[instance.type],
|
||||
'type': _$EmoteTypeEnumMap[instance.type]!,
|
||||
'ownerId': instance.ownerId,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -35,13 +35,18 @@ class IRCMessage {
|
|||
this.mention,
|
||||
});
|
||||
|
||||
/// Returns a list of messages where the gievn CLEARCHAT message is applied.
|
||||
static List<IRCMessage> clearChat({required List<IRCMessage> messages, required IRCMessage ircMessage}) {
|
||||
/// Applies the given CLEARCHAT message to the provided messages and buffer.
|
||||
static void clearChat({
|
||||
required List<IRCMessage> messages,
|
||||
required List<IRCMessage> bufferedMessages,
|
||||
required IRCMessage ircMessage,
|
||||
}) {
|
||||
// If there is no message, it means that entire chat was cleared.
|
||||
if (ircMessage.message == null) {
|
||||
messages.clear();
|
||||
bufferedMessages.clear();
|
||||
messages.add(IRCMessage.createNotice(message: 'Chat was cleared by a moderator'));
|
||||
return messages;
|
||||
return;
|
||||
}
|
||||
|
||||
final bannedUser = ircMessage.message;
|
||||
|
|
@ -60,11 +65,26 @@ class IRCMessage {
|
|||
}
|
||||
});
|
||||
|
||||
return messages;
|
||||
// Search the buffered messages for the banned/timed-out user.
|
||||
bufferedMessages.asMap().forEach((i, bufferedMessage) {
|
||||
if (bufferedMessage.user == bannedUser) {
|
||||
// Mark the message for removal.
|
||||
bufferedMessages[i].command = Command.clearChat;
|
||||
|
||||
// If timed-out, indicate the duration.
|
||||
if (banDuration != null) {
|
||||
bufferedMessages[i].tags['ban-duration'] = banDuration;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns a list of messages where the gievn CLEARMSG message is applied.
|
||||
static List<IRCMessage> clearMessage({required List<IRCMessage> messages, required IRCMessage ircMessage}) {
|
||||
/// Applies the given CLEARMSG message to the provided messages and buffer.
|
||||
static void clearMessage({
|
||||
required List<IRCMessage> messages,
|
||||
required List<IRCMessage> bufferedMessages,
|
||||
required IRCMessage ircMessage,
|
||||
}) {
|
||||
final targetId = ircMessage.tags['target-msg-id'];
|
||||
|
||||
// Search for the message associated with the ID and mark the message for deletion.
|
||||
|
|
@ -75,7 +95,13 @@ class IRCMessage {
|
|||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
// Search for the buffered message associated with the ID and mark the message for deletion.
|
||||
for (var i = 0; i < bufferedMessages.length; i++) {
|
||||
if (bufferedMessages[i].tags['id'] == targetId) {
|
||||
bufferedMessages[i].command = Command.clearMessage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an [InlineSpan] list that corresponds to the badges, username, words, and emotes of the given [IRCMessage].
|
||||
|
|
@ -86,6 +112,7 @@ class IRCMessage {
|
|||
required double emoteScale,
|
||||
required bool isLightTheme,
|
||||
required bool launchExternal,
|
||||
required void Function()? onLongPressName,
|
||||
bool showMessage = true,
|
||||
bool useZeroWidth = false,
|
||||
bool useReadableColors = false,
|
||||
|
|
@ -263,11 +290,16 @@ class IRCMessage {
|
|||
// debugPrint('NEW - NAME: ${tags['display-name']!}, HUE: ${hsl.hue}, SATURATION: ${hsl.saturation}, LIGHNTESS: ${hsl.lightness}');
|
||||
|
||||
span.add(
|
||||
TextSpan(
|
||||
text: tags['display-name']!,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
WidgetSpan(
|
||||
child: InkWell(
|
||||
onLongPress: onLongPressName,
|
||||
child: Text(
|
||||
tags['display-name']!,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -367,9 +399,7 @@ class IRCMessage {
|
|||
Column(
|
||||
children: [
|
||||
Text(
|
||||
nextWordIsEmoji
|
||||
? '${words[index]} - Emoji'
|
||||
: '${emoteStack.last.name} - ${emoteType[emoteStack.last.type.index]}',
|
||||
nextWordIsEmoji ? '${words[index]} - Emoji' : '${emoteStack.last.name} - ${emoteType[emoteStack.last.type.index]}',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:frosty/screens/channel/chat/emote_menu/emote_menu.dart';
|
|||
import 'package:frosty/screens/channel/chat/widgets/chat_bottom_bar.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
|
||||
class Chat extends StatelessWidget {
|
||||
|
|
@ -15,75 +16,99 @@ class Chat extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
return Stack(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (chatStore.assetsStore.showEmoteMenu) {
|
||||
chatStore.assetsStore.showEmoteMenu = false;
|
||||
} else if (chatStore.textFieldFocusNode.hasFocus) {
|
||||
chatStore.textFieldFocusNode.unfocus();
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: chatStore.settings.messageScale),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
addAutomaticKeepAlives: false,
|
||||
addRepaintBoundaries: false,
|
||||
itemCount: chatStore.messages.length,
|
||||
controller: chatStore.scrollController,
|
||||
itemBuilder: (context, index) => ChatMessage(
|
||||
ircMessage: chatStore.messages[index],
|
||||
chatStore: chatStore,
|
||||
),
|
||||
);
|
||||
},
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (chatStore.assetsStore.showEmoteMenu) {
|
||||
chatStore.assetsStore.showEmoteMenu = false;
|
||||
} else if (chatStore.textFieldFocusNode.hasFocus) {
|
||||
chatStore.textFieldFocusNode.unfocus();
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(textScaleFactor: chatStore.settings.messageScale),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: chatStore.settings.fontSize),
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return ListView.builder(
|
||||
reverse: true,
|
||||
padding: EdgeInsets.zero,
|
||||
addAutomaticKeepAlives: false,
|
||||
controller: chatStore.scrollController,
|
||||
itemCount: chatStore.renderMessages.length,
|
||||
itemBuilder: (context, index) => ChatMessage(
|
||||
ircMessage: chatStore.renderMessages.reversed.toList()[index],
|
||||
chatStore: chatStore,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
child: chatStore.autoScroll
|
||||
? null
|
||||
: Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
|
||||
onPressed: chatStore.resumeScroll,
|
||||
icon: const Icon(Icons.arrow_circle_down),
|
||||
child: const Text('Resume Scroll'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
child: chatStore.autoScroll
|
||||
? null
|
||||
: Button(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
|
||||
onPressed: chatStore.resumeScroll,
|
||||
icon: const Icon(Icons.arrow_circle_down),
|
||||
child: const Text('Resume Scroll'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore),
|
||||
AnimatedContainer(
|
||||
curve: Curves.ease,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: chatStore.assetsStore.showEmoteMenu ? MediaQuery.of(context).size.height / 3 : 0,
|
||||
child: AnimatedOpacity(
|
||||
curve: Curves.ease,
|
||||
opacity: chatStore.assetsStore.showEmoteMenu ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: EmoteMenu(chatStore: chatStore),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (chatStore.settings.showBottomBar) ChatBottomBar(chatStore: chatStore),
|
||||
AnimatedContainer(
|
||||
curve: Curves.ease,
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: chatStore.assetsStore.showEmoteMenu ? MediaQuery.of(context).size.height / 3 : 0,
|
||||
child: AnimatedOpacity(
|
||||
curve: Curves.ease,
|
||||
opacity: chatStore.assetsStore.showEmoteMenu ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: EmoteMenu(chatStore: chatStore),
|
||||
),
|
||||
onEnd: () => chatStore.scrollController.jumpTo(chatStore.scrollController.position.maxScrollExtent),
|
||||
child: chatStore.notification != null
|
||||
? Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
child: AlertMessage(
|
||||
message: chatStore.notification!,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:frosty/screens/channel/stores/chat_details_store.dart';
|
|||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/modal.dart';
|
||||
|
||||
class ChatDetails extends StatelessWidget {
|
||||
class ChatDetails extends StatefulWidget {
|
||||
final ChatDetailsStore chatDetailsStore;
|
||||
final ChatStore chatStore;
|
||||
final String userLogin;
|
||||
|
|
@ -18,6 +18,17 @@ class ChatDetails extends StatelessWidget {
|
|||
required this.userLogin,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ChatDetails> createState() => _ChatDetailsState();
|
||||
}
|
||||
|
||||
class _ChatDetailsState extends State<ChatDetails> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.chatDetailsStore.updateChatters();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FrostyModal(
|
||||
|
|
@ -30,13 +41,13 @@ class ChatDetails extends StatelessWidget {
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 15.0),
|
||||
child: ChatModes(roomState: chatDetailsStore.roomState),
|
||||
child: ChatModes(roomState: widget.chatDetailsStore.roomState),
|
||||
),
|
||||
Expanded(
|
||||
child: ChattersList(
|
||||
chatDetailsStore: chatDetailsStore,
|
||||
chatStore: chatStore,
|
||||
userLogin: userLogin,
|
||||
chatDetailsStore: widget.chatDetailsStore,
|
||||
chatStore: widget.chatStore,
|
||||
userLogin: widget.userLogin,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/models/irc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ChatModes extends StatelessWidget {
|
||||
final ROOMSTATE roomState;
|
||||
|
|
@ -11,10 +10,6 @@ class ChatModes extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final test = Intl.plural(2, other: 'test');
|
||||
|
||||
debugPrint(test);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:frosty/constants/constants.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
import 'package:frosty/widgets/button.dart';
|
||||
import 'package:frosty/widgets/dialog.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
|
|
@ -77,43 +78,49 @@ class _RecentEmotesPanelState extends State<RecentEmotesPanel> {
|
|||
],
|
||||
),
|
||||
),
|
||||
SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final emote = widget.chatStore.assetsStore.recentEmotes[index];
|
||||
final validEmotes = [...widget.chatStore.assetsStore.emoteToObject.values, ...widget.chatStore.assetsStore.userEmoteToObject.values];
|
||||
final matchingEmotes = validEmotes.where((existingEmote) => existingEmote.name == emote.name && existingEmote.type == emote.type);
|
||||
if (widget.chatStore.assetsStore.recentEmotes.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: AlertMessage(message: 'No recent emotes'),
|
||||
)
|
||||
else
|
||||
SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: MediaQuery.of(context).orientation == Orientation.portrait
|
||||
? 8
|
||||
: context.read<SettingsStore>().showVideo
|
||||
? 6
|
||||
: 16,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final emote = widget.chatStore.assetsStore.recentEmotes[index];
|
||||
final validEmotes = [...widget.chatStore.assetsStore.emoteToObject.values, ...widget.chatStore.assetsStore.userEmoteToObject.values];
|
||||
final matchingEmotes = validEmotes.where((existingEmote) => existingEmote.name == emote.name && existingEmote.type == emote.type);
|
||||
|
||||
return InkWell(
|
||||
onTap: matchingEmotes.isNotEmpty ? () => widget.chatStore.addEmote(emote) : null,
|
||||
child: Tooltip(
|
||||
message: emote.name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: matchingEmotes.isNotEmpty ? matchingEmotes.first.url : emote.url,
|
||||
color: matchingEmotes.isNotEmpty ? null : const Color.fromRGBO(255, 255, 255, 0.5),
|
||||
colorBlendMode: matchingEmotes.isNotEmpty ? null : BlendMode.modulate,
|
||||
height: emote.height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emote.width?.toDouble(),
|
||||
return InkWell(
|
||||
onTap: matchingEmotes.isNotEmpty ? () => widget.chatStore.addEmote(emote) : null,
|
||||
child: Tooltip(
|
||||
message: emote.name,
|
||||
preferBelow: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: matchingEmotes.isNotEmpty ? matchingEmotes.first.url : emote.url,
|
||||
color: matchingEmotes.isNotEmpty ? null : const Color.fromRGBO(255, 255, 255, 0.5),
|
||||
colorBlendMode: matchingEmotes.isNotEmpty ? null : BlendMode.modulate,
|
||||
height: emote.height?.toDouble() ?? defaultEmoteSize,
|
||||
width: emote.width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: widget.chatStore.assetsStore.recentEmotes.length,
|
||||
);
|
||||
},
|
||||
childCount: widget.chatStore.assetsStore.recentEmotes.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter_mobx/flutter_mobx.dart';
|
|||
import 'package:frosty/models/irc.dart';
|
||||
import 'package:frosty/screens/channel/chat/widgets/chat_user_modal.dart';
|
||||
import 'package:frosty/screens/channel/stores/chat_store.dart';
|
||||
import 'package:frosty/widgets/alert_message.dart';
|
||||
|
||||
class ChatMessage extends StatelessWidget {
|
||||
final IRCMessage ircMessage;
|
||||
|
|
@ -20,6 +19,36 @@ class ChatMessage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void onLongPressName() {
|
||||
// Ignore if the message is a recent message in the modal bottom sheet.
|
||||
if (isModal) return;
|
||||
|
||||
// Ignore if long-pressing own username.
|
||||
if (ircMessage.user == null || ircMessage.user == chatStore.auth.user.details?.login) return;
|
||||
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: chatStore,
|
||||
username: ircMessage.user!,
|
||||
userId: ircMessage.tags['user-id']!,
|
||||
displayName: ircMessage.tags['display-name']!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onLongPressMessage() {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
Clipboard.setData(ClipboardData(text: ircMessage.message));
|
||||
|
||||
chatStore.notification = 'Message copied';
|
||||
}
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
Color? color;
|
||||
|
|
@ -33,6 +62,7 @@ class ChatMessage extends StatelessWidget {
|
|||
renderMessage = Text.rich(
|
||||
TextSpan(
|
||||
children: ircMessage.generateSpan(
|
||||
onLongPressName: onLongPressName,
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
emoteScale: chatStore.settings.emoteScale,
|
||||
|
|
@ -58,6 +88,7 @@ class ChatMessage extends StatelessWidget {
|
|||
Text.rich(
|
||||
TextSpan(
|
||||
children: ircMessage.generateSpan(
|
||||
onLongPressName: onLongPressName,
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
emoteScale: chatStore.settings.emoteScale,
|
||||
|
|
@ -125,6 +156,7 @@ class ChatMessage extends StatelessWidget {
|
|||
Text.rich(
|
||||
TextSpan(
|
||||
children: ircMessage.generateSpan(
|
||||
onLongPressName: onLongPressName,
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
assetsStore: chatStore.assetsStore,
|
||||
emoteScale: chatStore.settings.emoteScale,
|
||||
|
|
@ -171,36 +203,7 @@ class ChatMessage extends StatelessWidget {
|
|||
FocusScope.of(context).unfocus();
|
||||
if (chatStore.assetsStore.showEmoteMenu) chatStore.assetsStore.showEmoteMenu = false;
|
||||
},
|
||||
onLongPress: isModal
|
||||
? () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
Clipboard.setData(ClipboardData(text: ircMessage.message));
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: AlertMessage(message: 'Message copied to clipboard'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
: ircMessage.user != null && ircMessage.user != chatStore.auth.user.details?.login
|
||||
? () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => ChatUserModal(
|
||||
chatStore: chatStore,
|
||||
username: ircMessage.user!,
|
||||
userId: ircMessage.tags['user-id']!,
|
||||
displayName: ircMessage.tags['display-name']!,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onLongPress: onLongPressMessage,
|
||||
child: coloredMessage,
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:frosty/screens/channel/stores/chat_assets_store.dart';
|
|||
import 'package:frosty/screens/channel/stores/chat_details_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
part 'chat_store.g.dart';
|
||||
|
|
@ -18,8 +19,11 @@ part 'chat_store.g.dart';
|
|||
class ChatStore = ChatStoreBase with _$ChatStore;
|
||||
|
||||
abstract class ChatStoreBase with Store {
|
||||
static const _messageUpperLimit = 7500;
|
||||
static const _messageLimit = 5000;
|
||||
/// The total maximum amount of messages in chat.
|
||||
static const _messageLimit = 10000;
|
||||
|
||||
/// The maximum ammount of messages to render when autoscroll is enabled.
|
||||
static const _renderMessageLimit = 100;
|
||||
|
||||
/// The provided auth store to determine login status, get the token, and use the headers for requests.
|
||||
final AuthStore auth;
|
||||
|
|
@ -63,9 +67,30 @@ abstract class ChatStoreBase with Store {
|
|||
/// Requested message to be sent by the user. Will only be sent on receipt of a USERNOTICE command.
|
||||
IRCMessage? toSend;
|
||||
|
||||
/// The list of chat messages to add once autoscroll is resumed.
|
||||
/// This is used as an optimization to prevent the list from being updated/shifted while the user is scrolling.
|
||||
final _messageBuffer = <IRCMessage>[];
|
||||
|
||||
/// The list of chat messages to render and display.
|
||||
@readonly
|
||||
var _messages = ObservableList<IRCMessage>();
|
||||
final messages = ObservableList<IRCMessage>();
|
||||
|
||||
/// The list of reaction disposer functions that will be used later when disposing.
|
||||
final reactions = <ReactionDisposer>[];
|
||||
|
||||
/// The periodic timer used for batching chat message re-renders.
|
||||
late final Timer _messageBufferTimer;
|
||||
|
||||
@computed
|
||||
List<IRCMessage> get renderMessages {
|
||||
// If autoscroll is disabled, render ALL messages in chat.
|
||||
// The second condition is to prevent an out of index error with sublist.
|
||||
if (!_autoScroll || messages.length < _renderMessageLimit) return messages;
|
||||
|
||||
// When autoscroll is enabled, only show the first [_renderMessageLimit] messages.
|
||||
// This will improve performance by only rendering a limited amount of messages
|
||||
// instead of the entire history at all times.
|
||||
return messages.sublist(messages.length - _renderMessageLimit);
|
||||
}
|
||||
|
||||
/// If the chat should automatically scroll/jump to the latest message.
|
||||
@readonly
|
||||
|
|
@ -87,7 +112,12 @@ abstract class ChatStoreBase with Store {
|
|||
@observable
|
||||
var expandChat = false;
|
||||
|
||||
final reactions = <ReactionDisposer>[];
|
||||
/// A notification message to display above the chat.
|
||||
@observable
|
||||
String? notification;
|
||||
|
||||
/// Timer used for dismissing the notification.
|
||||
Timer? _notificationTimer;
|
||||
|
||||
ChatStoreBase({
|
||||
required this.auth,
|
||||
|
|
@ -98,20 +128,38 @@ abstract class ChatStoreBase with Store {
|
|||
required this.channelId,
|
||||
required this.displayName,
|
||||
}) {
|
||||
// Enable wakelock to prevent the chat from sleeping.
|
||||
if (settings.chatOnlyPreventSleep) Wakelock.enable();
|
||||
|
||||
// Create a reaction that will reconnect to chat when logging in or out.
|
||||
// Closing the channel will trigger a reconnect with the new credentials.
|
||||
reactions.add(reaction(
|
||||
(_) => auth.isLoggedIn,
|
||||
(_) => _channel?.sink.close(1001),
|
||||
));
|
||||
reactions.add(
|
||||
reaction(
|
||||
(_) => auth.isLoggedIn,
|
||||
(_) => _channel?.sink.close(1001),
|
||||
),
|
||||
);
|
||||
|
||||
reactions.add(
|
||||
reaction(
|
||||
(_) => notification,
|
||||
(_) {
|
||||
if (_notificationTimer != null) _notificationTimer?.cancel();
|
||||
_notificationTimer = Timer(const Duration(seconds: 2), () => notification = null);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Create a timer that will add messages from the buffer every 200 milliseconds.
|
||||
_messageBufferTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) => addMessages());
|
||||
|
||||
assetsStore.init();
|
||||
chatDetailsStore.updateChatters();
|
||||
|
||||
_messages.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: 'Connecting to chat...'));
|
||||
|
||||
if (settings.chatDelay > 0) {
|
||||
_messages.add(IRCMessage.createNotice(
|
||||
_messageBuffer.add(IRCMessage.createNotice(
|
||||
message: 'Waiting ${settings.chatDelay.toInt()} ${settings.chatDelay == 1.0 ? 'second' : 'seconds'} due to message delay setting...'));
|
||||
}
|
||||
|
||||
|
|
@ -119,12 +167,12 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
// Tell the scrollController to determine when auto-scroll should be enabled or disabled.
|
||||
scrollController.addListener(() {
|
||||
// If the user scrolls up, auto-scroll will stop, allowing them to freely scroll back to previous messages.
|
||||
// Else if the user scrolls back to the bottom edge (latest message), auto-scroll will resume.
|
||||
if (scrollController.position.pixels < scrollController.position.maxScrollExtent) {
|
||||
if (_autoScroll == true) _autoScroll = false;
|
||||
} else if (scrollController.position.atEdge || scrollController.position.pixels > scrollController.position.maxScrollExtent) {
|
||||
if (_autoScroll == false) _autoScroll = true;
|
||||
// If the scroll position is at the latest message (maximum possible), enable autoscroll.
|
||||
// Else if the position is before the latest message (not at the edge), disable autoscroll.
|
||||
if (scrollController.position.pixels <= 0) {
|
||||
_autoScroll = true;
|
||||
} else if (scrollController.position.pixels > 0) {
|
||||
_autoScroll = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -161,7 +209,7 @@ abstract class ChatStoreBase with Store {
|
|||
// The IRC data can contain more than one message separated by CRLF.
|
||||
// To account for this, split by CRLF, then loop and process each message.
|
||||
for (final message in data.trimRight().split('\r\n')) {
|
||||
// debugPrint(message);
|
||||
// debugPrint('$message\n');
|
||||
if (message.startsWith('@')) {
|
||||
final parsedIRCMessage = IRCMessage.fromString(message, userLogin: auth.user.details?.login);
|
||||
|
||||
|
|
@ -172,25 +220,33 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
switch (parsedIRCMessage.command) {
|
||||
case Command.privateMessage:
|
||||
_messages.add(parsedIRCMessage);
|
||||
break;
|
||||
case Command.clearChat:
|
||||
_messages = IRCMessage.clearChat(messages: _messages, ircMessage: parsedIRCMessage).asObservable();
|
||||
break;
|
||||
case Command.clearMessage:
|
||||
_messages = IRCMessage.clearMessage(messages: _messages, ircMessage: parsedIRCMessage).asObservable();
|
||||
break;
|
||||
case Command.notice:
|
||||
case Command.userNotice:
|
||||
_messages.add(parsedIRCMessage);
|
||||
_messageBuffer.add(parsedIRCMessage);
|
||||
break;
|
||||
case Command.clearChat:
|
||||
IRCMessage.clearChat(
|
||||
messages: messages,
|
||||
bufferedMessages: _messageBuffer,
|
||||
ircMessage: parsedIRCMessage,
|
||||
);
|
||||
break;
|
||||
case Command.clearMessage:
|
||||
IRCMessage.clearMessage(
|
||||
messages: messages,
|
||||
bufferedMessages: _messageBuffer,
|
||||
ircMessage: parsedIRCMessage,
|
||||
);
|
||||
break;
|
||||
case Command.roomState:
|
||||
chatDetailsStore.roomState = chatDetailsStore.roomState.fromIRCMessage(parsedIRCMessage);
|
||||
continue;
|
||||
case Command.userState:
|
||||
_userState = _userState.fromIRCMessage(parsedIRCMessage);
|
||||
|
||||
if (toSend != null) {
|
||||
_messages.add(toSend!);
|
||||
textController.clear();
|
||||
_messageBuffer.add(toSend!);
|
||||
toSend = null;
|
||||
}
|
||||
break;
|
||||
|
|
@ -212,24 +268,13 @@ abstract class ChatStoreBase with Store {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (_autoScroll) {
|
||||
if (_messages.length >= _messageLimit) _messages.removeAt(0);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove messages when an upper limit is reached.
|
||||
// This will prevent an infinite amount of messages when autoscroll is off.
|
||||
if (_messages.length >= _messageUpperLimit) _messages.removeRange(0, 500);
|
||||
|
||||
// Hard upper-limit of 5000 messages to prevent infinite messages being added when scrolling.
|
||||
// If the message limit is reached, remove the oldest message.
|
||||
if (_autoScroll && messages.length >= _messageLimit) messages.removeRange(0, 500);
|
||||
} else if (message == 'PING :tmi.twitch.tv') {
|
||||
_channel?.sink.add('PONG :tmi.twitch.tv');
|
||||
return;
|
||||
} else if (message.contains('Welcome, GLHF!')) {
|
||||
_messages.add(IRCMessage.createNotice(message: "Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!"));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: "Connected to $displayName${regexEnglish.hasMatch(displayName) ? '' : ' ($channelName)'}'s chat!"));
|
||||
|
||||
getAssets();
|
||||
|
||||
|
|
@ -258,15 +303,15 @@ abstract class ChatStoreBase with Store {
|
|||
/// Re-enables [_autoScroll] and jumps to the latest message.
|
||||
@action
|
||||
void resumeScroll() {
|
||||
if (_messages.length >= _messageLimit) _messages.removeRange(0, _messages.length - _messageLimit);
|
||||
|
||||
_autoScroll = true;
|
||||
|
||||
// Jump to the latest message (bottom of the list/chat).
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
scrollController.jumpTo(0);
|
||||
|
||||
// Schedule a postFrameCallback in the event a new message is added at the same time.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => scrollController.jumpTo(scrollController.position.maxScrollExtent));
|
||||
// Add a post frame callback in the event a messages is added at the same time.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
scrollController.jumpTo(0);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -284,7 +329,7 @@ abstract class ChatStoreBase with Store {
|
|||
if (_backoffTime > 0) {
|
||||
// Add notice that chat was disconnected and then wait the backoff time before reconnecting.
|
||||
final notice = 'Disconnected from chat, waiting $_backoffTime ${_backoffTime == 1 ? 'second' : 'seconds'} before reconnecting...';
|
||||
_messages.add(IRCMessage.createNotice(message: notice));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: notice));
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(seconds: _backoffTime));
|
||||
|
|
@ -294,7 +339,7 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
// Increment the retry count and attempt the reconnect.
|
||||
_retries++;
|
||||
_messages.add(IRCMessage.createNotice(message: 'Reconnecting to chat (attempt $_retries)...'));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: 'Reconnecting to chat (attempt $_retries)...'));
|
||||
connectToChat();
|
||||
},
|
||||
);
|
||||
|
|
@ -321,6 +366,14 @@ abstract class ChatStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void addMessages() {
|
||||
if (!_autoScroll || _messageBuffer.isEmpty) return;
|
||||
|
||||
messages.addAll(_messageBuffer);
|
||||
_messageBuffer.clear();
|
||||
}
|
||||
|
||||
/// Sends the given string message by the logged-in user and adds it to [_messages].
|
||||
@action
|
||||
void sendMessage(String message) {
|
||||
|
|
@ -328,7 +381,7 @@ abstract class ChatStoreBase with Store {
|
|||
if (message.isEmpty) return;
|
||||
|
||||
if (_channel == null || _channel?.closeCode != null) {
|
||||
_messages.add(IRCMessage.createNotice(message: 'Failed to send message: disconnected from chat.'));
|
||||
_messageBuffer.add(IRCMessage.createNotice(message: 'Failed to send message: disconnected from chat.'));
|
||||
} else {
|
||||
// Send the message to the IRC chat room.
|
||||
_channel?.sink.add('PRIVMSG #$channelName :$message');
|
||||
|
|
@ -344,18 +397,11 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
final userChatMessage = IRCMessage.fromString(userStateString);
|
||||
userChatMessage.localEmotes?.addAll(assetsStore.userEmoteToObject);
|
||||
if (auth.isLoggedIn && auth.user.details != null) userChatMessage.tags['user-id'] = auth.user.details!.id;
|
||||
|
||||
toSend = userChatMessage;
|
||||
}
|
||||
|
||||
// Clear the previous input in the TextField.
|
||||
textController.clear();
|
||||
}
|
||||
|
||||
// Scroll to the latest message after sending.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (scrollController.hasClients) scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds the given [emote] to the chat textfield.
|
||||
|
|
@ -382,6 +428,8 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
/// Closes and disposes all the channels and controllers used by the store.
|
||||
void dispose() {
|
||||
_messageBufferTimer.cancel();
|
||||
|
||||
_channel?.sink.close(1001);
|
||||
_channel = null;
|
||||
|
||||
|
|
@ -395,5 +443,8 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
assetsStore.dispose();
|
||||
chatDetailsStore.dispose();
|
||||
|
||||
// Disable wakelock so that the sleep timer will function properly.
|
||||
Wakelock.disable();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,23 +9,13 @@ part of 'chat_store.dart';
|
|||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||
|
||||
mixin _$ChatStore on ChatStoreBase, Store {
|
||||
late final _$_messagesAtom =
|
||||
Atom(name: 'ChatStoreBase._messages', context: context);
|
||||
|
||||
ObservableList<IRCMessage> get messages {
|
||||
_$_messagesAtom.reportRead();
|
||||
return super._messages;
|
||||
}
|
||||
Computed<List<IRCMessage>>? _$renderMessagesComputed;
|
||||
|
||||
@override
|
||||
ObservableList<IRCMessage> get _messages => messages;
|
||||
|
||||
@override
|
||||
set _messages(ObservableList<IRCMessage> value) {
|
||||
_$_messagesAtom.reportWrite(value, super._messages, () {
|
||||
super._messages = value;
|
||||
});
|
||||
}
|
||||
List<IRCMessage> get renderMessages => (_$renderMessagesComputed ??=
|
||||
Computed<List<IRCMessage>>(() => super.renderMessages,
|
||||
name: 'ChatStoreBase.renderMessages'))
|
||||
.value;
|
||||
|
||||
late final _$_autoScrollAtom =
|
||||
Atom(name: 'ChatStoreBase._autoScroll', context: context);
|
||||
|
|
@ -135,6 +125,30 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$notificationAtom =
|
||||
Atom(name: 'ChatStoreBase.notification', context: context);
|
||||
|
||||
@override
|
||||
String? get notification {
|
||||
_$notificationAtom.reportRead();
|
||||
return super.notification;
|
||||
}
|
||||
|
||||
@override
|
||||
set notification(String? value) {
|
||||
_$notificationAtom.reportWrite(value, super.notification, () {
|
||||
super.notification = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$getAssetsAsyncAction =
|
||||
AsyncAction('ChatStoreBase.getAssets', context: context);
|
||||
|
||||
@override
|
||||
Future<void> getAssets() {
|
||||
return _$getAssetsAsyncAction.run(() => super.getAssets());
|
||||
}
|
||||
|
||||
late final _$ChatStoreBaseActionController =
|
||||
ActionController(name: 'ChatStoreBase', context: context);
|
||||
|
||||
|
|
@ -171,6 +185,17 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addMessages() {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
name: 'ChatStoreBase.addMessages');
|
||||
try {
|
||||
return super.addMessages();
|
||||
} finally {
|
||||
_$ChatStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void sendMessage(String message) {
|
||||
final _$actionInfo = _$ChatStoreBaseActionController.startAction(
|
||||
|
|
@ -196,7 +221,9 @@ mixin _$ChatStore on ChatStoreBase, Store {
|
|||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
expandChat: ${expandChat}
|
||||
expandChat: ${expandChat},
|
||||
notification: ${notification},
|
||||
renderMessages: ${renderMessages}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/api/twitch_api.dart';
|
||||
|
|
@ -18,45 +19,36 @@ class VideoStore = VideoStoreBase with _$VideoStore;
|
|||
abstract class VideoStoreBase with Store {
|
||||
final TwitchApi twitchApi;
|
||||
|
||||
/// The userlogin of the current channel.
|
||||
final String userLogin;
|
||||
|
||||
final AuthStore authStore;
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
/// The [Floating] instance used for initiating PiP on Android.
|
||||
final floating = Floating();
|
||||
|
||||
/// The webview controller used for injecting JavaScript to control the webview and video player.
|
||||
WebViewController? controller;
|
||||
|
||||
late Timer _overlayTimer;
|
||||
|
||||
/// The current timer for the sleep timer if active.
|
||||
Timer? sleepTimer;
|
||||
|
||||
@observable
|
||||
var sleepHours = 0;
|
||||
/// The timer that handles hiding the overlay automatically
|
||||
late Timer _overlayTimer;
|
||||
|
||||
@observable
|
||||
var sleepMinutes = 0;
|
||||
|
||||
@observable
|
||||
var timeRemaining = const Duration();
|
||||
|
||||
@readonly
|
||||
var _paused = true;
|
||||
|
||||
@readonly
|
||||
var _overlayVisible = true;
|
||||
|
||||
@readonly
|
||||
var _isIPad = false;
|
||||
|
||||
@readonly
|
||||
StreamTwitch? _streamInfo;
|
||||
|
||||
@computed
|
||||
String get videoUrl => settingsStore.showOverlay
|
||||
? 'https://player.twitch.tv/?channel=$userLogin&controls=false&muted=false&parent=frosty'
|
||||
: 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty';
|
||||
/// Disposes the overlay reactions.
|
||||
late final ReactionDisposer _disposeOverlayReaction;
|
||||
|
||||
/// The JavaScript channels used to communicate play/pause from the webview to Flutter.
|
||||
late final javascriptChannels = {
|
||||
JavascriptChannel(
|
||||
name: 'Pause',
|
||||
name: 'VideoPause',
|
||||
onMessageReceived: (message) => _paused = true,
|
||||
),
|
||||
JavascriptChannel(
|
||||
name: 'Play',
|
||||
name: 'VideoPlaying',
|
||||
onMessageReceived: (message) {
|
||||
_paused = false;
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].muted = false;');
|
||||
|
|
@ -65,93 +57,7 @@ abstract class VideoStoreBase with Store {
|
|||
),
|
||||
};
|
||||
|
||||
final String userLogin;
|
||||
final AuthStore authStore;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
VideoStoreBase({
|
||||
required this.userLogin,
|
||||
required this.twitchApi,
|
||||
required this.authStore,
|
||||
required this.settingsStore,
|
||||
}) {
|
||||
_overlayTimer = Timer(const Duration(seconds: 3), () => _overlayVisible = false);
|
||||
updateStreamInfo();
|
||||
}
|
||||
|
||||
@action
|
||||
void handlePausePlay() {
|
||||
try {
|
||||
if (_paused) {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].play();');
|
||||
} else {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].pause();');
|
||||
}
|
||||
|
||||
_paused = !_paused;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void handleVideoTap() {
|
||||
_overlayTimer.cancel();
|
||||
|
||||
if (_overlayVisible) {
|
||||
_overlayVisible = false;
|
||||
} else {
|
||||
updateStreamInfo();
|
||||
|
||||
_overlayVisible = true;
|
||||
_overlayTimer = Timer(const Duration(seconds: 5), () => _overlayVisible = false);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateStreamInfo() async {
|
||||
try {
|
||||
_streamInfo = await twitchApi.getStream(userLogin: userLogin, headers: authStore.headersTwitch);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
|
||||
_overlayTimer.cancel();
|
||||
_streamInfo = null;
|
||||
_paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void handleExpand() {
|
||||
settingsStore.expandInfo = !settingsStore.expandInfo;
|
||||
|
||||
_overlayTimer.cancel();
|
||||
_overlayTimer = Timer(const Duration(seconds: 5), () => _overlayVisible = false);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> handleToggleOverlay() async {
|
||||
if (settingsStore.toggleableOverlay) {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
settingsStore.showOverlay = !settingsStore.showOverlay;
|
||||
|
||||
await controller?.loadUrl(videoUrl);
|
||||
|
||||
if (settingsStore.showOverlay) {
|
||||
_overlayVisible = true;
|
||||
|
||||
_overlayTimer.cancel();
|
||||
_overlayTimer = Timer(const Duration(seconds: 3), () => _overlayVisible = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleRefresh() {
|
||||
HapticFeedback.lightImpact();
|
||||
controller?.reload();
|
||||
}
|
||||
|
||||
/// Used for preventing accidental navigation in the webview.
|
||||
FutureOr<NavigationDecision> handleNavigation(NavigationRequest navigation) {
|
||||
if (navigation.url.startsWith('https://player.twitch.tv')) {
|
||||
return NavigationDecision.navigate;
|
||||
|
|
@ -159,11 +65,67 @@ abstract class VideoStoreBase with Store {
|
|||
return NavigationDecision.prevent;
|
||||
}
|
||||
|
||||
/// The amount of hours the sleep timer is set to.
|
||||
@observable
|
||||
var sleepHours = 0;
|
||||
|
||||
/// The amount of minutes the sleep timer is set to.
|
||||
@observable
|
||||
var sleepMinutes = 0;
|
||||
|
||||
/// The time remaining for the sleep timer.
|
||||
@observable
|
||||
var timeRemaining = const Duration();
|
||||
|
||||
/// If the video is currently paused.
|
||||
///
|
||||
/// Does not pause or play the video, only used for rendering state of the overlay.
|
||||
@readonly
|
||||
var _paused = true;
|
||||
|
||||
/// If the overlay is should be visible.
|
||||
@readonly
|
||||
var _overlayVisible = true;
|
||||
|
||||
/// If the current device is iPad.
|
||||
@readonly
|
||||
var _isIPad = false;
|
||||
|
||||
/// The current stream info, used for displaying relevant info on the overlay.
|
||||
@readonly
|
||||
StreamTwitch? _streamInfo;
|
||||
|
||||
/// The video URL to use for the webview. Controls will be disabled when custom overlay is enabled.
|
||||
@computed
|
||||
String get videoUrl => settingsStore.showOverlay
|
||||
? 'https://player.twitch.tv/?channel=$userLogin&controls=false&muted=false&parent=frosty'
|
||||
: 'https://player.twitch.tv/?channel=$userLogin&muted=false&parent=frosty';
|
||||
|
||||
VideoStoreBase({
|
||||
required this.userLogin,
|
||||
required this.twitchApi,
|
||||
required this.authStore,
|
||||
required this.settingsStore,
|
||||
}) {
|
||||
// Initialize the [_overlayTimer] to hide the overlay automatically after 5 seconds.
|
||||
_overlayTimer = Timer(const Duration(seconds: 5), () => _overlayVisible = false);
|
||||
|
||||
// Initialize a reaction that will reload the webview whenever the overlay is toggled.
|
||||
_disposeOverlayReaction = reaction(
|
||||
(_) => settingsStore.showOverlay,
|
||||
(_) => controller?.loadUrl(videoUrl),
|
||||
);
|
||||
|
||||
updateStreamInfo();
|
||||
}
|
||||
|
||||
/// Initializes the video webview.
|
||||
@action
|
||||
Future<void> initVideo() async {
|
||||
// Add event listeners to notify the JavaScript channels when the video plays and pauses.
|
||||
try {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("pause", () => Pause.postMessage("video paused"));');
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("play", () => Play.postMessage("video playing"));');
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("pause", () => VideoPause.postMessage("video paused"));');
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].addEventListener("playing", () => VideoPlaying.postMessage("video playing"));');
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
|
|
@ -182,16 +144,78 @@ abstract class VideoStoreBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
void requestPictureInPicture() {
|
||||
/// Called whenever the video/overlay is tapped.
|
||||
@action
|
||||
void handleVideoTap() {
|
||||
_overlayTimer.cancel();
|
||||
|
||||
if (_overlayVisible) {
|
||||
_overlayVisible = false;
|
||||
} else {
|
||||
updateStreamInfo();
|
||||
|
||||
_overlayVisible = true;
|
||||
_overlayTimer = Timer(const Duration(seconds: 5), () => _overlayVisible = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the stream info from the Twitch API.
|
||||
///
|
||||
/// If the stream is offline, disables the overlay.
|
||||
@action
|
||||
Future<void> updateStreamInfo() async {
|
||||
try {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].requestPictureInPicture();');
|
||||
_streamInfo = await twitchApi.getStream(userLogin: userLogin, headers: authStore.headersTwitch);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
|
||||
_overlayTimer.cancel();
|
||||
_streamInfo = null;
|
||||
_paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles toggling "minimal mode" on the overlay.
|
||||
///
|
||||
/// "Minimal mode" is when only the channel name is visible on the bottom-left of the overlay.
|
||||
@action
|
||||
void handleExpand() {
|
||||
settingsStore.expandInfo = !settingsStore.expandInfo;
|
||||
|
||||
_overlayTimer.cancel();
|
||||
_overlayTimer = Timer(const Duration(seconds: 5), () => _overlayVisible = false);
|
||||
}
|
||||
|
||||
/// Handles the toggle overlay options.
|
||||
///
|
||||
/// The toggle overlay option allows switching between the custom and Twitch's overlay by long-pressing the overlay.
|
||||
@action
|
||||
void handleToggleOverlay() {
|
||||
if (settingsStore.toggleableOverlay) {
|
||||
HapticFeedback.mediumImpact();
|
||||
|
||||
settingsStore.showOverlay = !settingsStore.showOverlay;
|
||||
|
||||
if (settingsStore.showOverlay) {
|
||||
_overlayVisible = true;
|
||||
|
||||
_overlayTimer.cancel();
|
||||
_overlayTimer = Timer(const Duration(seconds: 3), () => _overlayVisible = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Refreshes the stream webview and updates the stream info.
|
||||
@action
|
||||
void handleRefresh() {
|
||||
HapticFeedback.lightImpact();
|
||||
controller?.reload();
|
||||
updateStreamInfo();
|
||||
}
|
||||
|
||||
/// Updates the sleep timer with [sleepHours] and [sleepMinutes].
|
||||
/// Calls [onTimerFinished] when the sleep timer completes.
|
||||
@action
|
||||
void updateSleepTimer({required void Function() onTimerFinished}) {
|
||||
// If hours and minutes are 0, do nothing.
|
||||
if (sleepHours == 0 && sleepMinutes == 0) return;
|
||||
|
|
@ -224,8 +248,46 @@ abstract class VideoStoreBase with Store {
|
|||
}
|
||||
|
||||
/// Cancels the sleep timer and resets the time remaining.
|
||||
@action
|
||||
void cancelSleepTimer() {
|
||||
sleepTimer?.cancel();
|
||||
timeRemaining = const Duration();
|
||||
}
|
||||
|
||||
/// Play or pause the video depending on the current state of [_paused].
|
||||
void handlePausePlay() {
|
||||
try {
|
||||
if (_paused) {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].play();');
|
||||
} else {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].pause();');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiate picture in picture if available.
|
||||
///
|
||||
/// On iOS, this will utilize the web picture-in-picture API.
|
||||
/// On Android, this will utilize the native Android PiP API.
|
||||
void requestPictureInPicture() {
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
floating.enable();
|
||||
} else if (Platform.isIOS) {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].requestPictureInPicture();');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void dispose() {
|
||||
controller?.runJavascript('document.getElementsByTagName("video")[0].pause();');
|
||||
_disposeOverlayReaction();
|
||||
floating.dispose();
|
||||
sleepTimer?.cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,23 +137,6 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$updateStreamInfoAsyncAction =
|
||||
AsyncAction('VideoStoreBase.updateStreamInfo', context: context);
|
||||
|
||||
@override
|
||||
Future<void> updateStreamInfo() {
|
||||
return _$updateStreamInfoAsyncAction.run(() => super.updateStreamInfo());
|
||||
}
|
||||
|
||||
late final _$handleToggleOverlayAsyncAction =
|
||||
AsyncAction('VideoStoreBase.handleToggleOverlay', context: context);
|
||||
|
||||
@override
|
||||
Future<void> handleToggleOverlay() {
|
||||
return _$handleToggleOverlayAsyncAction
|
||||
.run(() => super.handleToggleOverlay());
|
||||
}
|
||||
|
||||
late final _$initVideoAsyncAction =
|
||||
AsyncAction('VideoStoreBase.initVideo', context: context);
|
||||
|
||||
|
|
@ -162,20 +145,17 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
return _$initVideoAsyncAction.run(() => super.initVideo());
|
||||
}
|
||||
|
||||
late final _$VideoStoreBaseActionController =
|
||||
ActionController(name: 'VideoStoreBase', context: context);
|
||||
late final _$updateStreamInfoAsyncAction =
|
||||
AsyncAction('VideoStoreBase.updateStreamInfo', context: context);
|
||||
|
||||
@override
|
||||
void handlePausePlay() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.handlePausePlay');
|
||||
try {
|
||||
return super.handlePausePlay();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
Future<void> updateStreamInfo() {
|
||||
return _$updateStreamInfoAsyncAction.run(() => super.updateStreamInfo());
|
||||
}
|
||||
|
||||
late final _$VideoStoreBaseActionController =
|
||||
ActionController(name: 'VideoStoreBase', context: context);
|
||||
|
||||
@override
|
||||
void handleVideoTap() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
|
|
@ -198,6 +178,61 @@ mixin _$VideoStore on VideoStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleToggleOverlay() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.handleToggleOverlay');
|
||||
try {
|
||||
return super.handleToggleOverlay();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleRefresh() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.handleRefresh');
|
||||
try {
|
||||
return super.handleRefresh();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSleepTimer({required void Function() onTimerFinished}) {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.updateSleepTimer');
|
||||
try {
|
||||
return super.updateSleepTimer(onTimerFinished: onTimerFinished);
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void cancelSleepTimer() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.cancelSleepTimer');
|
||||
try {
|
||||
return super.cancelSleepTimer();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final _$actionInfo = _$VideoStoreBaseActionController.startAction(
|
||||
name: 'VideoStoreBase.dispose');
|
||||
try {
|
||||
return super.dispose();
|
||||
} finally {
|
||||
_$VideoStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
|
@ -88,6 +89,12 @@ class _VideoChatState extends State<VideoChat> {
|
|||
|
||||
final overlay = GestureDetector(
|
||||
onLongPress: _videoStore.handleToggleOverlay,
|
||||
onDoubleTap: () {
|
||||
if (MediaQuery.of(context).orientation == Orientation.portrait) return;
|
||||
|
||||
// Double tap to toggle fullscreen in landscape mode.
|
||||
settingsStore.fullScreen = !settingsStore.fullScreen;
|
||||
},
|
||||
onTap: () {
|
||||
if (_chatStore.assetsStore.showEmoteMenu) {
|
||||
_chatStore.assetsStore.showEmoteMenu = false;
|
||||
|
|
@ -160,14 +167,9 @@ class _VideoChatState extends State<VideoChat> {
|
|||
],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
final videoChat = Scaffold(
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
// Scroll to bottom when summoning keyboard or rotating.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_chatStore.scrollController.hasClients) _chatStore.scrollController.jumpTo(_chatStore.scrollController.position.maxScrollExtent);
|
||||
});
|
||||
|
||||
if (orientation == Orientation.landscape) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
|
||||
|
|
@ -199,6 +201,11 @@ class _VideoChatState extends State<VideoChat> {
|
|||
color: settingsStore.showVideo ? Colors.black : Theme.of(context).scaffoldBackgroundColor,
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
left:
|
||||
(settingsStore.landscapeCutout == LandscapeCutoutType.both || settingsStore.landscapeCutout == LandscapeCutoutType.left) ? false : true,
|
||||
right: (settingsStore.landscapeCutout == LandscapeCutoutType.both || settingsStore.landscapeCutout == LandscapeCutoutType.right)
|
||||
? false
|
||||
: true,
|
||||
child: settingsStore.showVideo
|
||||
? settingsStore.fullScreen
|
||||
? Stack(
|
||||
|
|
@ -246,13 +253,24 @@ class _VideoChatState extends State<VideoChat> {
|
|||
},
|
||||
),
|
||||
);
|
||||
|
||||
// If on Android, use PiPSwitcher to enable PiP functionality.
|
||||
if (Platform.isAndroid) {
|
||||
return PiPSwitcher(
|
||||
floating: _videoStore.floating,
|
||||
childWhenEnabled: player,
|
||||
childWhenDisabled: videoChat,
|
||||
);
|
||||
}
|
||||
|
||||
return videoChat;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_chatStore.dispose();
|
||||
|
||||
_videoStore.cancelSleepTimer();
|
||||
_videoStore.dispose();
|
||||
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
|
|
@ -614,7 +632,7 @@ class _VideoOverlay extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (Platform.isIOS && videoStore.settingsStore.pictureInPicture)
|
||||
if (videoStore.settingsStore.pictureInPicture)
|
||||
IconButton(
|
||||
tooltip: 'Picture-in-picture',
|
||||
icon: const Icon(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ abstract class CategoriesStoreBase with Store {
|
|||
}
|
||||
|
||||
// Fetches the top categories based on the current cursor.
|
||||
@action
|
||||
Future<void> getCategories() async {
|
||||
_isLoading = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,14 @@ mixin _$CategoriesStore on CategoriesStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$getCategoriesAsyncAction =
|
||||
AsyncAction('CategoriesStoreBase.getCategories', context: context);
|
||||
|
||||
@override
|
||||
Future<void> getCategories() {
|
||||
return _$getCategoriesAsyncAction.run(() => super.getCategories());
|
||||
}
|
||||
|
||||
late final _$CategoriesStoreBaseActionController =
|
||||
ActionController(name: 'CategoriesStoreBase', context: context);
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ abstract class ListStoreBase with Store {
|
|||
}
|
||||
|
||||
/// Fetches the streams based on the type and current cursor.
|
||||
@action
|
||||
Future<void> getStreams() async {
|
||||
_isLoading = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,14 @@ mixin _$ListStore on ListStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$getStreamsAsyncAction =
|
||||
AsyncAction('ListStoreBase.getStreams', context: context);
|
||||
|
||||
@override
|
||||
Future<void> getStreams() {
|
||||
return _$getStreamsAsyncAction.run(() => super.getStreams());
|
||||
}
|
||||
|
||||
late final _$ListStoreBaseActionController =
|
||||
ActionController(name: 'ListStoreBase', context: context);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const landscapeCutouts = ['None', 'Left', 'Right', 'Both'];
|
||||
|
||||
const timestamps = ['Disabled', '12-hour', '24-hour'];
|
||||
|
||||
const sectionPadding = EdgeInsets.only(left: 16.0, bottom: 5.0, top: 30.0);
|
||||
|
|
@ -40,14 +42,6 @@ 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 while typing.'),
|
||||
value: settingsStore.autocomplete,
|
||||
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.autocomplete = newValue : null,
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
|
|
@ -71,6 +65,20 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
],
|
||||
),
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Prevent sleep in chat-only mode'),
|
||||
subtitle: const Text('Requires restarting the chat in order to take effect.'),
|
||||
value: settingsStore.chatOnlyPreventSleep,
|
||||
onChanged: !settingsStore.showVideo ? (newValue) => settingsStore.chatOnlyPreventSleep = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Autocomplete'),
|
||||
subtitle: const Text('Shows a bar that suggests matching emotes and mentions while typing.'),
|
||||
value: settingsStore.autocomplete,
|
||||
onChanged: settingsStore.showBottomBar ? (newValue) => settingsStore.autocomplete = newValue : null,
|
||||
),
|
||||
const SectionHeader(
|
||||
'Layout',
|
||||
padding: sectionPadding,
|
||||
|
|
@ -86,6 +94,21 @@ class _ChatSettingsState extends State<ChatSettings> {
|
|||
value: settingsStore.landscapeChatLeftSide,
|
||||
onChanged: (newValue) => settingsStore.landscapeChatLeftSide = newValue,
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
title: const Text('Landscape fill cutout side'),
|
||||
subtitle: const Text('Overrides and fills the available space in the display cutout/notch.'),
|
||||
trailing: DropdownButton(
|
||||
value: settingsStore.landscapeCutout,
|
||||
onChanged: (LandscapeCutoutType? newValue) => settingsStore.landscapeCutout = newValue!,
|
||||
items: LandscapeCutoutType.values
|
||||
.map((LandscapeCutoutType value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(landscapeCutouts[value.index]),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
ListTile(
|
||||
title: Row(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
|
|
@ -34,14 +32,13 @@ class VideoSettings extends StatelessWidget {
|
|||
value: settingsStore.showOverlay,
|
||||
onChanged: settingsStore.showVideo ? (newValue) => settingsStore.showOverlay = newValue : null,
|
||||
),
|
||||
if (Platform.isIOS)
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Picture-in-picture Button (experimental)'),
|
||||
subtitle: const Text('Adds a button to enter PiP mode on the bottom right of the overlay (may cause freezes/crashes).'),
|
||||
value: settingsStore.pictureInPicture,
|
||||
onChanged: settingsStore.showVideo && settingsStore.showOverlay ? (newValue) => settingsStore.pictureInPicture = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Picture-in-picture Button (experimental)'),
|
||||
subtitle: const Text('Adds a button to enter PiP mode on the bottom right of the overlay (may cause freezes/crashes).'),
|
||||
value: settingsStore.pictureInPicture,
|
||||
onChanged: settingsStore.showVideo && settingsStore.showOverlay ? (newValue) => settingsStore.pictureInPicture = newValue : null,
|
||||
),
|
||||
SwitchListTile.adaptive(
|
||||
isThreeLine: true,
|
||||
title: const Text('Long-press player to toggle overlay'),
|
||||
|
|
|
|||
|
|
@ -85,8 +85,10 @@ abstract class _SettingsStoreBase with Store {
|
|||
}
|
||||
|
||||
// * Chat Settings
|
||||
static const defaultAutocomplete = true;
|
||||
static const defaultChatDelay = 0.0;
|
||||
static const defaultChatOnlyPreventSleep = true;
|
||||
static const defaultAutocomplete = true;
|
||||
static const defaultLandscapeCutout = LandscapeCutoutType.none;
|
||||
|
||||
static const defaultShowBottomBar = true;
|
||||
static const defaultLandscapeChatLeftSide = false;
|
||||
|
|
@ -106,14 +108,18 @@ abstract class _SettingsStoreBase with Store {
|
|||
static const defaultMessageSpacing = 10.0;
|
||||
static const defaultFontSize = 12.0;
|
||||
|
||||
@JsonKey(defaultValue: defaultAutocomplete)
|
||||
@observable
|
||||
var autocomplete = defaultAutocomplete;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatDelay)
|
||||
@observable
|
||||
var chatDelay = defaultChatDelay;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatOnlyPreventSleep)
|
||||
@observable
|
||||
var chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
|
||||
|
||||
@JsonKey(defaultValue: defaultAutocomplete)
|
||||
@observable
|
||||
var autocomplete = defaultAutocomplete;
|
||||
|
||||
@JsonKey(defaultValue: defaultShowBottomBar)
|
||||
@observable
|
||||
var showBottomBar = defaultShowBottomBar;
|
||||
|
|
@ -122,6 +128,10 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var landscapeChatLeftSide = defaultLandscapeChatLeftSide;
|
||||
|
||||
@JsonKey(defaultValue: defaultLandscapeCutout)
|
||||
@observable
|
||||
var landscapeCutout = defaultLandscapeCutout;
|
||||
|
||||
@JsonKey(defaultValue: defaultChatWidth)
|
||||
@observable
|
||||
var chatWidth = defaultChatWidth;
|
||||
|
|
@ -172,8 +182,9 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
@action
|
||||
void resetChatSettings() {
|
||||
autocomplete = defaultAutocomplete;
|
||||
chatDelay = defaultChatDelay;
|
||||
chatOnlyPreventSleep = defaultChatOnlyPreventSleep;
|
||||
autocomplete = defaultAutocomplete;
|
||||
showBottomBar = defaultShowBottomBar;
|
||||
landscapeChatLeftSide = defaultLandscapeChatLeftSide;
|
||||
chatWidth = defaultChatWidth;
|
||||
|
|
@ -228,6 +239,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
|
||||
@action
|
||||
void resetAllSettings() {
|
||||
resetGeneralSettings();
|
||||
resetVideoSettings();
|
||||
resetChatSettings();
|
||||
resetOtherSettings();
|
||||
|
|
@ -247,3 +259,10 @@ enum TimestampType {
|
|||
twelve,
|
||||
twentyFour,
|
||||
}
|
||||
|
||||
enum LandscapeCutoutType {
|
||||
none,
|
||||
left,
|
||||
right,
|
||||
both,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,14 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..toggleableOverlay = json['toggleableOverlay'] as bool? ?? false
|
||||
..pictureInPicture = json['pictureInPicture'] as bool? ?? false
|
||||
..overlayOpacity = (json['overlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
..autocomplete = json['autocomplete'] as bool? ?? true
|
||||
..chatDelay = (json['chatDelay'] as num?)?.toDouble() ?? 0.0
|
||||
..chatOnlyPreventSleep = json['chatOnlyPreventSleep'] as bool? ?? true
|
||||
..autocomplete = json['autocomplete'] as bool? ?? true
|
||||
..showBottomBar = json['showBottomBar'] as bool? ?? true
|
||||
..landscapeChatLeftSide = json['landscapeChatLeftSide'] as bool? ?? false
|
||||
..landscapeCutout = $enumDecodeNullable(
|
||||
_$LandscapeCutoutTypeEnumMap, json['landscapeCutout']) ??
|
||||
LandscapeCutoutType.none
|
||||
..chatWidth = (json['chatWidth'] as num?)?.toDouble() ?? 0.3
|
||||
..fullScreenChatOverlayOpacity =
|
||||
(json['fullScreenChatOverlayOpacity'] as num?)?.toDouble() ?? 0.5
|
||||
|
|
@ -48,7 +52,7 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
|
||||
Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
||||
<String, dynamic>{
|
||||
'themeType': _$ThemeTypeEnumMap[instance.themeType],
|
||||
'themeType': _$ThemeTypeEnumMap[instance.themeType]!,
|
||||
'showThumbnails': instance.showThumbnails,
|
||||
'largeStreamCard': instance.largeStreamCard,
|
||||
'showThumbnailUptime': instance.showThumbnailUptime,
|
||||
|
|
@ -58,17 +62,20 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'toggleableOverlay': instance.toggleableOverlay,
|
||||
'pictureInPicture': instance.pictureInPicture,
|
||||
'overlayOpacity': instance.overlayOpacity,
|
||||
'autocomplete': instance.autocomplete,
|
||||
'chatDelay': instance.chatDelay,
|
||||
'chatOnlyPreventSleep': instance.chatOnlyPreventSleep,
|
||||
'autocomplete': instance.autocomplete,
|
||||
'showBottomBar': instance.showBottomBar,
|
||||
'landscapeChatLeftSide': instance.landscapeChatLeftSide,
|
||||
'landscapeCutout':
|
||||
_$LandscapeCutoutTypeEnumMap[instance.landscapeCutout]!,
|
||||
'chatWidth': instance.chatWidth,
|
||||
'fullScreenChatOverlayOpacity': instance.fullScreenChatOverlayOpacity,
|
||||
'showZeroWidth': instance.showZeroWidth,
|
||||
'useReadableColors': instance.useReadableColors,
|
||||
'showDeletedMessages': instance.showDeletedMessages,
|
||||
'showChatMessageDividers': instance.showChatMessageDividers,
|
||||
'timestampType': _$TimestampTypeEnumMap[instance.timestampType],
|
||||
'timestampType': _$TimestampTypeEnumMap[instance.timestampType]!,
|
||||
'badgeScale': instance.badgeScale,
|
||||
'emoteScale': instance.emoteScale,
|
||||
'messageScale': instance.messageScale,
|
||||
|
|
@ -87,6 +94,13 @@ const _$ThemeTypeEnumMap = {
|
|||
ThemeType.black: 'black',
|
||||
};
|
||||
|
||||
const _$LandscapeCutoutTypeEnumMap = {
|
||||
LandscapeCutoutType.none: 'none',
|
||||
LandscapeCutoutType.left: 'left',
|
||||
LandscapeCutoutType.right: 'right',
|
||||
LandscapeCutoutType.both: 'both',
|
||||
};
|
||||
|
||||
const _$TimestampTypeEnumMap = {
|
||||
TimestampType.disabled: 'disabled',
|
||||
TimestampType.twelve: 'twelve',
|
||||
|
|
@ -260,22 +274,6 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$autocompleteAtom =
|
||||
Atom(name: '_SettingsStoreBase.autocomplete', context: context);
|
||||
|
||||
@override
|
||||
bool get autocomplete {
|
||||
_$autocompleteAtom.reportRead();
|
||||
return super.autocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
set autocomplete(bool value) {
|
||||
_$autocompleteAtom.reportWrite(value, super.autocomplete, () {
|
||||
super.autocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatDelayAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatDelay', context: context);
|
||||
|
||||
|
|
@ -292,6 +290,39 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$chatOnlyPreventSleepAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatOnlyPreventSleep', context: context);
|
||||
|
||||
@override
|
||||
bool get chatOnlyPreventSleep {
|
||||
_$chatOnlyPreventSleepAtom.reportRead();
|
||||
return super.chatOnlyPreventSleep;
|
||||
}
|
||||
|
||||
@override
|
||||
set chatOnlyPreventSleep(bool value) {
|
||||
_$chatOnlyPreventSleepAtom.reportWrite(value, super.chatOnlyPreventSleep,
|
||||
() {
|
||||
super.chatOnlyPreventSleep = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$autocompleteAtom =
|
||||
Atom(name: '_SettingsStoreBase.autocomplete', context: context);
|
||||
|
||||
@override
|
||||
bool get autocomplete {
|
||||
_$autocompleteAtom.reportRead();
|
||||
return super.autocomplete;
|
||||
}
|
||||
|
||||
@override
|
||||
set autocomplete(bool value) {
|
||||
_$autocompleteAtom.reportWrite(value, super.autocomplete, () {
|
||||
super.autocomplete = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showBottomBarAtom =
|
||||
Atom(name: '_SettingsStoreBase.showBottomBar', context: context);
|
||||
|
||||
|
|
@ -325,6 +356,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$landscapeCutoutAtom =
|
||||
Atom(name: '_SettingsStoreBase.landscapeCutout', context: context);
|
||||
|
||||
@override
|
||||
LandscapeCutoutType get landscapeCutout {
|
||||
_$landscapeCutoutAtom.reportRead();
|
||||
return super.landscapeCutout;
|
||||
}
|
||||
|
||||
@override
|
||||
set landscapeCutout(LandscapeCutoutType value) {
|
||||
_$landscapeCutoutAtom.reportWrite(value, super.landscapeCutout, () {
|
||||
super.landscapeCutout = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$chatWidthAtom =
|
||||
Atom(name: '_SettingsStoreBase.chatWidth', context: context);
|
||||
|
||||
|
|
@ -667,10 +714,12 @@ showOverlay: ${showOverlay},
|
|||
toggleableOverlay: ${toggleableOverlay},
|
||||
pictureInPicture: ${pictureInPicture},
|
||||
overlayOpacity: ${overlayOpacity},
|
||||
autocomplete: ${autocomplete},
|
||||
chatDelay: ${chatDelay},
|
||||
chatOnlyPreventSleep: ${chatOnlyPreventSleep},
|
||||
autocomplete: ${autocomplete},
|
||||
showBottomBar: ${showBottomBar},
|
||||
landscapeChatLeftSide: ${landscapeChatLeftSide},
|
||||
landscapeCutout: ${landscapeCutout},
|
||||
chatWidth: ${chatWidth},
|
||||
fullScreenChatOverlayOpacity: ${fullScreenChatOverlayOpacity},
|
||||
showZeroWidth: ${showZeroWidth},
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import 'package:flutter/material.dart';
|
|||
class AlertMessage extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final Color? color;
|
||||
|
||||
const AlertMessage({
|
||||
Key? key,
|
||||
required this.message,
|
||||
final this.icon = Icons.info,
|
||||
this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
|
@ -18,15 +20,15 @@ class AlertMessage extends StatelessWidget {
|
|||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.grey,
|
||||
color: color ?? Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
Flexible(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey,
|
||||
color: color ?? Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class _ButtonState extends State<Button> with SingleTickerProviderStateMixin {
|
|||
onPrimary: widget.fill ? null : widget.color,
|
||||
padding: widget.padding,
|
||||
splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,
|
||||
textStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: widget.fontSize),
|
||||
textStyle: TextStyle(fontWeight: FontWeight.w600, fontSize: widget.fontSize),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30.0)),
|
||||
elevation: widget.color == null ? 10.0 : 0.0,
|
||||
).copyWith(
|
||||
|
|
|
|||
98
pubspec.lock
98
pubspec.lock
|
|
@ -7,21 +7,21 @@ packages:
|
|||
name: _fe_analyzer_shared
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "41.0.0"
|
||||
version: "43.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "4.3.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.3.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -56,7 +56,7 @@ packages:
|
|||
name: build_config
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -77,7 +77,7 @@ packages:
|
|||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
version: "2.2.0"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -98,7 +98,7 @@ packages:
|
|||
name: built_value
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.3.3"
|
||||
version: "8.4.0"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -154,7 +154,7 @@ packages:
|
|||
name: code_builder
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
version: "4.2.0"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -196,7 +196,7 @@ packages:
|
|||
name: device_info_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.0.1"
|
||||
device_info_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -231,7 +231,7 @@ packages:
|
|||
name: device_info_plus_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -245,7 +245,7 @@ packages:
|
|||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "2.0.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -260,6 +260,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
floating:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: floating
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -306,7 +313,7 @@ packages:
|
|||
name: flutter_native_splash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
version: "2.2.6"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -449,21 +456,21 @@ packages:
|
|||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.5.0"
|
||||
version: "4.6.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
version: "6.3.1"
|
||||
lint:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lint
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
version: "1.10.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -512,7 +519,7 @@ packages:
|
|||
name: mobx
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.7+4"
|
||||
version: "2.0.7+5"
|
||||
mobx_codegen:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -547,7 +554,7 @@ packages:
|
|||
name: package_info_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
version: "1.4.3"
|
||||
package_info_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -582,7 +589,7 @@ packages:
|
|||
name: package_info_plus_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -617,14 +624,14 @@ packages:
|
|||
name: path_provider_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.16"
|
||||
version: "2.0.17"
|
||||
path_provider_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.10"
|
||||
version: "2.0.11"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -652,7 +659,7 @@ packages:
|
|||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.1.0"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -729,21 +736,21 @@ packages:
|
|||
name: rxdart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.27.4"
|
||||
version: "0.27.5"
|
||||
sentry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sentry
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.6.2"
|
||||
version: "6.9.0"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.6.2"
|
||||
version: "6.9.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -806,7 +813,7 @@ packages:
|
|||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -937,7 +944,7 @@ packages:
|
|||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "6.1.5"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1001,6 +1008,41 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
wakelock:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.2"
|
||||
wakelock_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
wakelock_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
wakelock_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
wakelock_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1028,7 +1070,7 @@ packages:
|
|||
name: webview_flutter_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.14"
|
||||
version: "2.9.2"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1042,14 +1084,14 @@ packages:
|
|||
name: webview_flutter_wkwebview
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.1"
|
||||
version: "2.9.2"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "2.7.0"
|
||||
xdg_directories:
|
||||
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: 2.0.0+25
|
||||
version: 2.1.0+26
|
||||
|
||||
environment:
|
||||
sdk: ">=2.15.0 <3.0.0"
|
||||
|
|
@ -29,7 +29,7 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
json_annotation: ^4.5.0
|
||||
json_annotation: ^4.6.0
|
||||
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
|
@ -54,6 +54,8 @@ dependencies:
|
|||
collection: ^1.16.0
|
||||
flutter_cache_manager: ^3.3.0
|
||||
device_info_plus: ^4.0.0
|
||||
floating: ^1.1.0
|
||||
wakelock: ^0.6.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:frosty/models/emotes.dart';
|
||||
|
||||
|
|
@ -284,7 +285,6 @@ void main() {
|
|||
expect(emote.visibilitySimple, ['GLOBAL']);
|
||||
expect(emote.mime, 'image/gif');
|
||||
expect(emote.status, 3);
|
||||
expect(emote.tags, []);
|
||||
expect(emote.width, [32, 48, 76, 128]);
|
||||
expect(emote.height, [32, 48, 76, 128]);
|
||||
|
||||
|
|
@ -383,7 +383,6 @@ void main() {
|
|||
expect(emote.visibilitySimple, []);
|
||||
expect(emote.mime, 'image/png');
|
||||
expect(emote.status, 3);
|
||||
expect(emote.tags, []);
|
||||
expect(emote.width, [32, 48, 76, 128]);
|
||||
expect(emote.height, [32, 48, 76, 128]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue