diff --git a/lib/models/badges.g.dart b/lib/models/badges.g.dart index 411bf390..49ccec05 100644 --- a/lib/models/badges.g.dart +++ b/lib/models/badges.g.dart @@ -6,17 +6,20 @@ part of 'badges.dart'; // JsonSerializableGenerator // ************************************************************************** -BadgeImagesTwitch _$BadgeImagesTwitchFromJson(Map json) => - BadgeImagesTwitch( - json['id'] as String, - json['image_url_1x'] as String, - json['image_url_2x'] as String, - json['image_url_4x'] as String, - ); +BadgeImagesTwitch _$BadgeImagesTwitchFromJson(Map json) { + return BadgeImagesTwitch( + json['id'] as String, + json['image_url_1x'] as String, + json['image_url_2x'] as String, + json['image_url_4x'] as String, + ); +} -BadgesTwitch _$BadgesTwitchFromJson(Map json) => BadgesTwitch( - json['set_id'] as String, - (json['versions'] as List) - .map((e) => BadgeImagesTwitch.fromJson(e as Map)) - .toList(), - ); +BadgesTwitch _$BadgesTwitchFromJson(Map json) { + return BadgesTwitch( + json['set_id'] as String, + (json['versions'] as List) + .map((e) => BadgeImagesTwitch.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/models/channel.g.dart b/lib/models/channel.g.dart index 2b55a8c8..69c3ca24 100644 --- a/lib/models/channel.g.dart +++ b/lib/models/channel.g.dart @@ -6,20 +6,22 @@ part of 'channel.dart'; // JsonSerializableGenerator // ************************************************************************** -Channel _$ChannelFromJson(Map json) => Channel( - id: json['id'] as String, - userId: json['user_id'] as String, - userLogin: json['user_login'] as String, - userName: json['user_name'] as String, - gameId: json['game_id'] as String, - gameName: json['game_name'] as String, - type: json['type'] as String, - title: json['title'] as String, - viewerCount: json['viewer_count'] as int, - startedAt: json['started_at'] as String, - language: json['language'] as String, - thumbnailUrl: json['thumbnail_url'] as String, - tagIds: - (json['tag_ids'] as List?)?.map((e) => e as String).toList(), - isMature: json['is_mature'] as bool, - ); +Channel _$ChannelFromJson(Map json) { + return Channel( + id: json['id'] as String, + userId: json['user_id'] as String, + userLogin: json['user_login'] as String, + userName: json['user_name'] as String, + gameId: json['game_id'] as String, + gameName: json['game_name'] as String, + type: json['type'] as String, + title: json['title'] as String, + viewerCount: json['viewer_count'] as int, + startedAt: json['started_at'] as String, + language: json['language'] as String, + thumbnailUrl: json['thumbnail_url'] as String, + tagIds: + (json['tag_ids'] as List?)?.map((e) => e as String).toList(), + isMature: json['is_mature'] as bool, + ); +} diff --git a/lib/models/emotes.dart b/lib/models/emotes.dart index 967fb883..d440e12c 100644 --- a/lib/models/emotes.dart +++ b/lib/models/emotes.dart @@ -2,6 +2,8 @@ import 'package:json_annotation/json_annotation.dart'; part 'emotes.g.dart'; +// TODO: Add 7TV emotes: https://github.com/SevenTV/ServerGo/blob/master/docs/rest-api.md + // * Twitch Emotes * @JsonSerializable(createToJson: false) class ImagesTwitch { diff --git a/lib/models/emotes.g.dart b/lib/models/emotes.g.dart index 2f2185d4..cba58a69 100644 --- a/lib/models/emotes.g.dart +++ b/lib/models/emotes.g.dart @@ -6,113 +6,134 @@ part of 'emotes.dart'; // JsonSerializableGenerator // ************************************************************************** -ImagesTwitch _$ImagesTwitchFromJson(Map json) => ImagesTwitch( - json['url_1x'] as String, - json['url_2x'] as String, - json['url_4x'] as String, - ); +ImagesTwitch _$ImagesTwitchFromJson(Map json) { + return ImagesTwitch( + json['url_1x'] as String, + json['url_2x'] as String, + json['url_4x'] as String, + ); +} -EmoteTwitch _$EmoteTwitchFromJson(Map json) => EmoteTwitch( - json['id'] as String, - json['name'] as String, - ImagesTwitch.fromJson(json['images'] as Map), - json['tier'] as String?, - json['emote_type'] as String?, - json['emote_set_id'] as String?, - (json['format'] as List).map((e) => e as String).toList(), - (json['scale'] as List).map((e) => e as String).toList(), - (json['theme_mode'] as List).map((e) => e as String).toList(), - ); +EmoteTwitch _$EmoteTwitchFromJson(Map json) { + return EmoteTwitch( + json['id'] as String, + json['name'] as String, + ImagesTwitch.fromJson(json['images'] as Map), + json['tier'] as String?, + json['emote_type'] as String?, + json['emote_set_id'] as String?, + (json['format'] as List).map((e) => e as String).toList(), + (json['scale'] as List).map((e) => e as String).toList(), + (json['theme_mode'] as List).map((e) => e as String).toList(), + ); +} -EmoteBTTVGlobal _$EmoteBTTVGlobalFromJson(Map json) => - EmoteBTTVGlobal( - json['id'] as String, - json['code'] as String, - json['imageType'] as String, - json['userId'] as String, - ); +EmoteBTTVGlobal _$EmoteBTTVGlobalFromJson(Map json) { + return EmoteBTTVGlobal( + json['id'] as String, + json['code'] as String, + json['imageType'] as String, + json['userId'] as String, + ); +} -UserBTTV _$UserBTTVFromJson(Map json) => UserBTTV( - json['id'] as String, - json['name'] as String, - json['displayName'] as String, - json['providerId'] as String, - ); +UserBTTV _$UserBTTVFromJson(Map json) { + return UserBTTV( + json['id'] as String, + json['name'] as String, + json['displayName'] as String, + json['providerId'] as String, + ); +} -EmoteBTTVShared _$EmoteBTTVSharedFromJson(Map json) => - EmoteBTTVShared( - json['id'] as String, - json['code'] as String, - json['imageType'] as String, - UserBTTV.fromJson(json['user'] as Map), - ); +EmoteBTTVShared _$EmoteBTTVSharedFromJson(Map json) { + return EmoteBTTVShared( + json['id'] as String, + json['code'] as String, + json['imageType'] as String, + UserBTTV.fromJson(json['user'] as Map), + ); +} -EmoteBTTVChannel _$EmoteBTTVChannelFromJson(Map json) => - EmoteBTTVChannel( - json['id'] as String, - (json['bots'] as List).map((e) => e as String).toList(), - (json['channelEmotes'] as List) - .map((e) => EmoteBTTVGlobal.fromJson(e as Map)) - .toList(), - (json['sharedEmotes'] as List) - .map((e) => EmoteBTTVShared.fromJson(e as Map)) - .toList(), - ); +EmoteBTTVChannel _$EmoteBTTVChannelFromJson(Map json) { + return EmoteBTTVChannel( + json['id'] as String, + (json['bots'] as List).map((e) => e as String).toList(), + (json['channelEmotes'] as List) + .map((e) => EmoteBTTVGlobal.fromJson(e as Map)) + .toList(), + (json['sharedEmotes'] as List) + .map((e) => EmoteBTTVShared.fromJson(e as Map)) + .toList(), + ); +} -UserFFZ _$UserFFZFromJson(Map json) => UserFFZ( - json['id'] as int, - json['name'] as String, - json['displayName'] as String, - ); +UserFFZ _$UserFFZFromJson(Map json) { + return UserFFZ( + json['id'] as int, + json['name'] as String, + json['displayName'] as String, + ); +} -ImagesFFZ _$ImagesFFZFromJson(Map json) => ImagesFFZ( - json['1x'] as String, - json['2x'] as String?, - json['4x'] as String?, - ); +ImagesFFZ _$ImagesFFZFromJson(Map json) { + return ImagesFFZ( + json['1x'] as String, + json['2x'] as String?, + json['4x'] as String?, + ); +} -EmoteFFZ _$EmoteFFZFromJson(Map json) => EmoteFFZ( - json['id'] as int, - UserFFZ.fromJson(json['user'] as Map), - json['code'] as String, - ImagesFFZ.fromJson(json['images'] as Map), - json['imageType'] as String, - ); +EmoteFFZ _$EmoteFFZFromJson(Map json) { + return EmoteFFZ( + json['id'] as int, + UserFFZ.fromJson(json['user'] as Map), + json['code'] as String, + ImagesFFZ.fromJson(json['images'] as Map), + json['imageType'] as String, + ); +} -Role7TV _$Role7TVFromJson(Map json) => Role7TV( - json['id'] as String, - json['name'] as String, - json['position'] as int, - json['color'] as int, - json['allowed'] as int, - json['denied'] as int, - json['default'] as bool?, - ); +Role7TV _$Role7TVFromJson(Map json) { + return Role7TV( + json['id'] as String, + json['name'] as String, + json['position'] as int, + json['color'] as int, + json['allowed'] as int, + json['denied'] as int, + json['default'] as bool?, + ); +} -Owner7TV _$Owner7TVFromJson(Map json) => Owner7TV( - json['id'] as String, - json['twitch_id'] as String, - json['login'] as String, - json['display_name'] as String, - Role7TV.fromJson(json['role'] as Map), - ); +Owner7TV _$Owner7TVFromJson(Map json) { + return Owner7TV( + json['id'] as String, + json['twitch_id'] as String, + json['login'] as String, + json['display_name'] as String, + Role7TV.fromJson(json['role'] as Map), + ); +} -Emote7TV _$Emote7TVFromJson(Map json) => Emote7TV( - json['id'] as String, - json['name'] as String, - json['owner'] == null - ? null - : Owner7TV.fromJson(json['owner'] as Map), - json['visibility'] as int, - (json['visibility_simple'] as List) - .map((e) => e as String) - .toList(), - json['mime'] as String, - json['status'] as int, - (json['tags'] as List).map((e) => e as String).toList(), - (json['width'] as List).map((e) => e as int).toList(), - (json['height'] as List).map((e) => e as int).toList(), - (json['urls'] as List) - .map((e) => (e as List).map((e) => e as String).toList()) - .toList(), - ); +Emote7TV _$Emote7TVFromJson(Map json) { + return Emote7TV( + json['id'] as String, + json['name'] as String, + json['owner'] == null + ? null + : Owner7TV.fromJson(json['owner'] as Map), + json['visibility'] as int, + (json['visibility_simple'] as List) + .map((e) => e as String) + .toList(), + json['mime'] as String, + json['status'] as int, + (json['tags'] as List).map((e) => e as String).toList(), + (json['width'] as List).map((e) => e as int).toList(), + (json['height'] as List).map((e) => e as int).toList(), + (json['urls'] as List) + .map((e) => (e as List).map((e) => e as String).toList()) + .toList(), + ); +} diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index 124fc2f3..60dc364c 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -6,15 +6,17 @@ part of 'user.dart'; // JsonSerializableGenerator // ************************************************************************** -UserTwitch _$UserTwitchFromJson(Map json) => UserTwitch( - json['id'] as String, - json['login'] as String, - json['display_name'] as String, - json['type'] as String, - json['broadcaster_type'] as String, - json['description'] as String, - json['profile_image_url'] as String, - json['offline_image_url'] as String, - json['view_count'] as int, - json['created_at'] as String, - ); +UserTwitch _$UserTwitchFromJson(Map json) { + return UserTwitch( + json['id'] as String, + json['login'] as String, + json['display_name'] as String, + json['type'] as String, + json['broadcaster_type'] as String, + json['description'] as String, + json['profile_image_url'] as String, + json['offline_image_url'] as String, + json['view_count'] as int, + json['created_at'] as String, + ); +} diff --git a/lib/providers/chat_provider.dart b/lib/providers/chat_provider.dart index 691b158e..899c748c 100644 --- a/lib/providers/chat_provider.dart +++ b/lib/providers/chat_provider.dart @@ -1,4 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; import 'package:frosty/models/channel.dart'; import 'package:frosty/providers/authentication_provider.dart'; @@ -11,6 +12,7 @@ class ChatProvider extends ChangeNotifier { final channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443')); final messages = []; + final scrollController = ScrollController(); final _assetToUrl = {}; final _emoteIdToWord = {}; @@ -31,6 +33,16 @@ class ChatProvider extends ChangeNotifier { for (final command in commands) { channel.sink.add(command); } + + scrollController.addListener(() { + if (!scrollController.position.atEdge) { + autoScroll = false; + notifyListeners(); + } else if (scrollController.position.atEdge && scrollController.position.pixels != scrollController.position.minScrollExtent) { + autoScroll = true; + notifyListeners(); + } + }); } Future getEmotes() async { @@ -96,17 +108,16 @@ class ChatProvider extends ChangeNotifier { case 'GLOBALUSERSTATE': break; case 'PRIVMSG': - if (messages.length >= 200) { - debugPrint('remove'); - messages.removeRange(0, 20); - } final message = splitMessage.sublist(3).join(' ').substring(1); - messages - ..add(const SizedBox(height: 10)) - ..add(ChatMessage( - key: Key(mappedTags['id']!), - children: privateMessage(tags: mappedTags, chatMessage: message), - )); + messages.add(const SizedBox(height: 10)); + messages.add(ChatMessage( + children: privateMessage(tags: mappedTags, chatMessage: message), + )); + if (autoScroll) { + SchedulerBinding.instance?.addPostFrameCallback((_) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + }); + } break; case 'ROOMSTATE': break; @@ -210,6 +221,14 @@ class ChatProvider extends ChangeNotifier { return result; } + void resumeScroll() { + autoScroll = true; + scrollController.jumpTo(scrollController.position.maxScrollExtent); + SchedulerBinding.instance?.addPostFrameCallback((_) { + scrollController.jumpTo(scrollController.position.maxScrollExtent); + }); + } + @override void dispose() { channel.sink.close(); diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index 4288a272..4faa5d47 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -12,29 +12,50 @@ class Chat extends StatelessWidget { Widget build(BuildContext context) { debugPrint('build'); final viewModel = context.read(); - return FutureBuilder( - future: viewModel.getEmotes(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return StreamBuilder( - stream: viewModel.channel.stream, - builder: (context, snapshot) { - viewModel.handleWebsocketData(snapshot.data); - if (viewModel.messages.isNotEmpty) { - return ListView( - reverse: true, - padding: const EdgeInsets.all(5.0), - children: viewModel.messages.reversed.toList(), + return SafeArea( + child: FutureBuilder( + future: viewModel.getEmotes(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return StreamBuilder( + stream: viewModel.channel.stream, + builder: (context, snapshot) { + viewModel.handleWebsocketData(snapshot.data); + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + ListView.builder( + addAutomaticKeepAlives: false, + addRepaintBoundaries: false, + physics: const ClampingScrollPhysics(), + itemCount: viewModel.messages.length, + controller: viewModel.scrollController, + padding: const EdgeInsets.all(5.0), + itemBuilder: (context, index) { + return viewModel.messages[index]; + }, + ), + Consumer( + builder: (context, viewModel, child) { + return Visibility( + visible: !viewModel.autoScroll, + child: ElevatedButton( + onPressed: () => viewModel.resumeScroll(), + child: const Text('Resume Scroll'), + ), + ); + }, + ), + ], ); - } - return const SizedBox(); - }, + }, + ); + } + return const Center( + child: CircularProgressIndicator(), ); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, + }, + ), ); } } diff --git a/lib/widgets/video.dart b/lib/widgets/video.dart index 3a3b752f..add884d8 100644 --- a/lib/widgets/video.dart +++ b/lib/widgets/video.dart @@ -9,7 +9,7 @@ class Video extends StatelessWidget { @override Widget build(BuildContext context) { return WebView( - initialUrl: 'https://player.twitch.tv/?channel=$channelName&muted=false&parent=frosty', + initialUrl: 'https://player.twitch.tv/?channel=$channelName&muted=false&parent=localhost.com', javascriptMode: JavascriptMode.unrestricted, allowsInlineMediaPlayback: true, initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, diff --git a/pubspec.lock b/pubspec.lock index d241ba62..63206503 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,21 +7,21 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "26.0.0" + version: "22.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "1.7.2" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.2.0" async: dependency: transitive description: @@ -323,7 +323,7 @@ packages: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.0.0" http: dependency: "direct main" description: @@ -379,7 +379,7 @@ packages: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "5.0.1" + version: "4.1.4" lints: dependency: transitive description: @@ -393,7 +393,7 @@ packages: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.1" matcher: dependency: transitive description: @@ -435,7 +435,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.0" path: dependency: transitive description: @@ -449,14 +449,14 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.3" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.0.2" path_provider_macos: dependency: transitive description: @@ -526,14 +526,14 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.0.0" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.0.0" rxdart: dependency: transitive description: @@ -566,14 +566,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" - source_helper: - dependency: transitive - description: - name: source_helper - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" + version: "1.0.3" source_span: dependency: transitive description: @@ -692,7 +685,7 @@ packages: name: webview_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.0.14" + version: "2.0.13" win32: dependency: transitive description: @@ -715,5 +708,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=2.13.0 <3.0.0" + flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1d824fcb..b33aeb99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,7 +54,7 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^1.0.0 build_runner: ^2.1.1 - json_serializable: ^5.0.1 + json_serializable: ^4.1.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec