From b86b989507a8ae775b26aa8ff2087f6d010dea15 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 1 Oct 2021 00:08:19 -0400 Subject: [PATCH 01/15] Convert providers to MobX stores --- lib/providers/settings_provider.dart | 3 - .../auth_store.dart} | 15 +- lib/stores/auth_store.g.dart | 33 +++++ .../channel_list_store.dart} | 65 ++++++--- lib/stores/channel_list_store.g.dart | 135 ++++++++++++++++++ .../chat_store.dart} | 75 +++++----- lib/stores/chat_store.g.dart | 58 ++++++++ 7 files changed, 320 insertions(+), 64 deletions(-) delete mode 100644 lib/providers/settings_provider.dart rename lib/{providers/authentication_provider.dart => stores/auth_store.dart} (94%) create mode 100644 lib/stores/auth_store.g.dart rename lib/{providers/channel_list_provider.dart => stores/channel_list_store.dart} (66%) create mode 100644 lib/stores/channel_list_store.g.dart rename lib/{providers/chat_provider.dart => stores/chat_store.dart} (94%) create mode 100644 lib/stores/chat_store.g.dart diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart deleted file mode 100644 index ba7cf13c..00000000 --- a/lib/providers/settings_provider.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -class SettingsProvider extends ChangeNotifier {} diff --git a/lib/providers/authentication_provider.dart b/lib/stores/auth_store.dart similarity index 94% rename from lib/providers/authentication_provider.dart rename to lib/stores/auth_store.dart index 9850452c..d114d713 100644 --- a/lib/providers/authentication_provider.dart +++ b/lib/stores/auth_store.dart @@ -1,3 +1,4 @@ +import 'package:mobx/mobx.dart'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:frosty/constants.dart'; @@ -6,7 +7,11 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; import 'package:flutter_web_auth/flutter_web_auth.dart'; -class AuthenticationProvider extends ChangeNotifier { +part 'auth_store.g.dart'; + +class AuthStore = AuthBase with _$AuthStore; + +abstract class AuthBase with Store { static String? _token; static String? get token => _token; @@ -16,7 +21,9 @@ class AuthenticationProvider extends ChangeNotifier { final _secret = secret; final _clientId = clientId; + @observable var _isLoggedIn = false; + bool get isLoggedIn => _isLoggedIn; var _tokenIsValid = false; @@ -85,7 +92,7 @@ class AuthenticationProvider extends ChangeNotifier { debugPrint('Token invalidated :('); _tokenIsValid = false; - notifyListeners(); + // notifyListeners(); } Future _getUserInfo() async { @@ -121,7 +128,7 @@ class AuthenticationProvider extends ChangeNotifier { await _getUserInfo(); - notifyListeners(); + // notifyListeners(); } catch (error) { debugPrint('Login failed due to $error'); } @@ -133,6 +140,6 @@ class AuthenticationProvider extends ChangeNotifier { await _storage.delete(key: 'USER_TOKEN'); debugPrint('Succesfully logged out'); - notifyListeners(); + // notifyListeners(); } } diff --git a/lib/stores/auth_store.g.dart b/lib/stores/auth_store.g.dart new file mode 100644 index 00000000..652e77bc --- /dev/null +++ b/lib/stores/auth_store.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// 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 + +mixin _$AuthStore on AuthBase, Store { + final _$_isLoggedInAtom = Atom(name: 'AuthBase._isLoggedIn'); + + @override + bool get _isLoggedIn { + _$_isLoggedInAtom.reportRead(); + return super._isLoggedIn; + } + + @override + set _isLoggedIn(bool value) { + _$_isLoggedInAtom.reportWrite(value, super._isLoggedIn, () { + super._isLoggedIn = value; + }); + } + + @override + String toString() { + return ''' + + '''; + } +} diff --git a/lib/providers/channel_list_provider.dart b/lib/stores/channel_list_store.dart similarity index 66% rename from lib/providers/channel_list_provider.dart rename to lib/stores/channel_list_store.dart index 820d33df..15452a3d 100644 --- a/lib/providers/channel_list_provider.dart +++ b/lib/stores/channel_list_store.dart @@ -1,22 +1,35 @@ +import 'package:mobx/mobx.dart'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:frosty/constants.dart'; -import 'package:frosty/providers/authentication_provider.dart'; +import 'package:frosty/stores/auth_store.dart'; import 'package:http/http.dart' as http; import 'package:frosty/models/channel.dart'; -class ChannelListProvider extends ChangeNotifier { - var topChannels = []; - var followedChannels = []; - var isLoading = false; +part 'channel_list_store.g.dart'; +class ChannelListStore = _ChannelListBase with _$ChannelListStore; + +abstract class _ChannelListBase with Store { + @observable + ObservableList topChannels = ObservableList(); + + @observable + ObservableList followedChannels = ObservableList(); + + @observable + bool isLoading = false; + + @observable String? topChannelsCurrentCursor; + + @observable String? followedChannelsCurrentCursor; - final token = AuthenticationProvider.token; + final token = AuthBase.token; final String? id; - ChannelListProvider({this.id}) { + _ChannelListBase({this.id}) { if (token != null) { updateTopChannels(); } @@ -43,6 +56,7 @@ class ChannelListProvider extends ChangeNotifier { } } + @action Future update({required Category category}) async { switch (category) { case Category.top: @@ -55,6 +69,7 @@ class ChannelListProvider extends ChangeNotifier { } /// Returns the top 10 streamers and a cursor for further requests. + @action Future updateTopChannels() async { final url = Uri.parse('https://api.twitch.tv/helix/streams?first=10'); final headers = {'Authorization': 'Bearer $token', 'Client-Id': clientId}; @@ -65,26 +80,32 @@ class ChannelListProvider extends ChangeNotifier { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; + final result = { + 'channels': data.map((channel) => Channel.fromJson(channel)).toList(), + 'cursor': decoded['pagination']['cursor'] + }; - topChannels = result['channels']; + topChannels = ObservableList.of(result['channels']); topChannelsCurrentCursor = result['cursor']; } else { debugPrint('Failed to update top channels'); } - notifyListeners(); + // notifyListeners(); } + @action Future getMoreChannels({required Category category}) async { isLoading = true; Uri url; switch (category) { case Category.top: - url = Uri.parse('https://api.twitch.tv/helix/streams?first=10&after=$topChannelsCurrentCursor'); + url = Uri.parse( + 'https://api.twitch.tv/helix/streams?first=10&after=$topChannelsCurrentCursor'); break; case Category.followed: - url = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id&first=10&after=$followedChannelsCurrentCursor'); + url = Uri.parse( + 'https://api.twitch.tv/helix/streams/followed?user_id=$id&first=10&after=$followedChannelsCurrentCursor'); break; } @@ -95,7 +116,10 @@ class ChannelListProvider extends ChangeNotifier { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; + final result = { + 'channels': data.map((channel) => Channel.fromJson(channel)).toList(), + 'cursor': decoded['pagination']['cursor'] + }; switch (category) { case Category.top: @@ -112,11 +136,13 @@ class ChannelListProvider extends ChangeNotifier { debugPrint('Failed to get more channels'); } isLoading = false; - notifyListeners(); + // notifyListeners(); } + @action Future updateFollowedChannels() async { - final url = Uri.parse('https://api.twitch.tv/helix/streams/followed?first=10&user_id=$id'); + final url = Uri.parse( + 'https://api.twitch.tv/helix/streams/followed?first=10&user_id=$id'); final headers = {'Authorization': 'Bearer $token', 'Client-Id': clientId}; final response = await http.get(url, headers: headers); @@ -125,14 +151,17 @@ class ChannelListProvider extends ChangeNotifier { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; + final result = { + 'channels': data.map((channel) => Channel.fromJson(channel)).toList(), + 'cursor': decoded['pagination']['cursor'] + }; - followedChannels = result['channels']; + followedChannels = ObservableList.of(result['channels']); followedChannelsCurrentCursor = result['cursor']; } else { debugPrint('Failed to update followed channels'); } - notifyListeners(); + // notifyListeners(); } } diff --git a/lib/stores/channel_list_store.g.dart b/lib/stores/channel_list_store.g.dart new file mode 100644 index 00000000..4cf0274c --- /dev/null +++ b/lib/stores/channel_list_store.g.dart @@ -0,0 +1,135 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'channel_list_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// 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 + +mixin _$ChannelListStore on _ChannelListBase, Store { + final _$topChannelsAtom = Atom(name: '_ChannelListBase.topChannels'); + + @override + ObservableList get topChannels { + _$topChannelsAtom.reportRead(); + return super.topChannels; + } + + @override + set topChannels(ObservableList value) { + _$topChannelsAtom.reportWrite(value, super.topChannels, () { + super.topChannels = value; + }); + } + + final _$followedChannelsAtom = + Atom(name: '_ChannelListBase.followedChannels'); + + @override + ObservableList get followedChannels { + _$followedChannelsAtom.reportRead(); + return super.followedChannels; + } + + @override + set followedChannels(ObservableList value) { + _$followedChannelsAtom.reportWrite(value, super.followedChannels, () { + super.followedChannels = value; + }); + } + + final _$isLoadingAtom = Atom(name: '_ChannelListBase.isLoading'); + + @override + bool get isLoading { + _$isLoadingAtom.reportRead(); + return super.isLoading; + } + + @override + set isLoading(bool value) { + _$isLoadingAtom.reportWrite(value, super.isLoading, () { + super.isLoading = value; + }); + } + + final _$topChannelsCurrentCursorAtom = + Atom(name: '_ChannelListBase.topChannelsCurrentCursor'); + + @override + String? get topChannelsCurrentCursor { + _$topChannelsCurrentCursorAtom.reportRead(); + return super.topChannelsCurrentCursor; + } + + @override + set topChannelsCurrentCursor(String? value) { + _$topChannelsCurrentCursorAtom + .reportWrite(value, super.topChannelsCurrentCursor, () { + super.topChannelsCurrentCursor = value; + }); + } + + final _$followedChannelsCurrentCursorAtom = + Atom(name: '_ChannelListBase.followedChannelsCurrentCursor'); + + @override + String? get followedChannelsCurrentCursor { + _$followedChannelsCurrentCursorAtom.reportRead(); + return super.followedChannelsCurrentCursor; + } + + @override + set followedChannelsCurrentCursor(String? value) { + _$followedChannelsCurrentCursorAtom + .reportWrite(value, super.followedChannelsCurrentCursor, () { + super.followedChannelsCurrentCursor = value; + }); + } + + final _$updateAsyncAction = AsyncAction('_ChannelListBase.update'); + + @override + Future update({required Category category}) { + return _$updateAsyncAction.run(() => super.update(category: category)); + } + + final _$updateTopChannelsAsyncAction = + AsyncAction('_ChannelListBase.updateTopChannels'); + + @override + Future updateTopChannels() { + return _$updateTopChannelsAsyncAction.run(() => super.updateTopChannels()); + } + + final _$getMoreChannelsAsyncAction = + AsyncAction('_ChannelListBase.getMoreChannels'); + + @override + Future getMoreChannels({required Category category}) { + return _$getMoreChannelsAsyncAction + .run(() => super.getMoreChannels(category: category)); + } + + final _$updateFollowedChannelsAsyncAction = + AsyncAction('_ChannelListBase.updateFollowedChannels'); + + @override + Future updateFollowedChannels() { + return _$updateFollowedChannelsAsyncAction + .run(() => super.updateFollowedChannels()); + } + + @override + String toString() { + return ''' +topChannels: ${topChannels}, +followedChannels: ${followedChannels}, +isLoading: ${isLoading}, +topChannelsCurrentCursor: ${topChannelsCurrentCursor}, +followedChannelsCurrentCursor: ${followedChannelsCurrentCursor} + '''; + } +} diff --git a/lib/providers/chat_provider.dart b/lib/stores/chat_store.dart similarity index 94% rename from lib/providers/chat_provider.dart rename to lib/stores/chat_store.dart index 9629b4d8..73f958d1 100644 --- a/lib/providers/chat_provider.dart +++ b/lib/stores/chat_store.dart @@ -2,50 +2,30 @@ 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'; +import 'package:frosty/stores/auth_store.dart'; import 'package:frosty/utility/request.dart'; import 'package:frosty/widgets/chat_message.dart'; +import 'package:mobx/mobx.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; -class ChatProvider extends ChangeNotifier { - final Channel channelInfo; +part 'chat_store.g.dart'; + +class ChatStore = _ChatStoreBase with _$ChatStore; + +abstract class _ChatStoreBase with Store { + @observable + bool autoScroll = true; + + List messages = []; final channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443')); - final messages = []; - final scrollController = ScrollController(); final _assetToUrl = {}; final _emoteIdToWord = {}; - var autoScroll = true; + final scrollController = ScrollController(); - ChatProvider({required this.channelInfo}) { - final commands = [ - 'PASS oauth:${AuthenticationProvider.token}', - 'NICK justinfan888', - 'CAP REQ :twitch.tv/tags', - 'CAP REQ :twitch.tv/commands', - // 'CAP REQ :twitch.tv/membership', - 'CAP END', - 'JOIN #${channelInfo.userLogin}', - ]; - - 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 { + Future start(Channel channelInfo) async { final assets = [ await Request.getEmotesBTTVGlobal(), await Request.getEmotesBTTVChannel(id: channelInfo.userId), @@ -64,8 +44,30 @@ class ChatProvider extends ChangeNotifier { _assetToUrl.addAll(map); } } + final commands = [ + 'PASS oauth:${AuthBase.token}', + 'NICK justinfan888', + 'CAP REQ :twitch.tv/tags', + 'CAP REQ :twitch.tv/commands', + // 'CAP REQ :twitch.tv/membership', + 'CAP END', + 'JOIN #${channelInfo.userLogin}', + ]; + + for (final command in commands) { + channel.sink.add(command); + } + + scrollController.addListener(() { + if (!scrollController.position.atEdge) { + autoScroll = false; + } else if (scrollController.position.atEdge && scrollController.position.pixels != scrollController.position.minScrollExtent) { + autoScroll = true; + } + }); } + @action void handleWebsocketData(Object? data) { for (final message in data.toString().split('\r\n')) { if (message.startsWith('@')) { @@ -223,6 +225,7 @@ class ChatProvider extends ChangeNotifier { return result; } + @action void resumeScroll() { autoScroll = true; scrollController.jumpTo(scrollController.position.maxScrollExtent); @@ -230,12 +233,6 @@ class ChatProvider extends ChangeNotifier { scrollController.jumpTo(scrollController.position.maxScrollExtent); }); } - - @override - void dispose() { - channel.sink.close(); - super.dispose(); - } } // https://stackoverflow.com/questions/50081213/how-do-i-use-hexadecimal-color-strings-in-flutter diff --git a/lib/stores/chat_store.g.dart b/lib/stores/chat_store.g.dart new file mode 100644 index 00000000..77fd94fd --- /dev/null +++ b/lib/stores/chat_store.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// 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 + +mixin _$ChatStore on _ChatStoreBase, Store { + final _$autoScrollAtom = Atom(name: '_ChatStoreBase.autoScroll'); + + @override + bool get autoScroll { + _$autoScrollAtom.reportRead(); + return super.autoScroll; + } + + @override + set autoScroll(bool value) { + _$autoScrollAtom.reportWrite(value, super.autoScroll, () { + super.autoScroll = value; + }); + } + + final _$_ChatStoreBaseActionController = + ActionController(name: '_ChatStoreBase'); + + @override + void handleWebsocketData(Object? data) { + final _$actionInfo = _$_ChatStoreBaseActionController.startAction( + name: '_ChatStoreBase.handleWebsocketData'); + try { + return super.handleWebsocketData(data); + } finally { + _$_ChatStoreBaseActionController.endAction(_$actionInfo); + } + } + + @override + void resumeScroll() { + final _$actionInfo = _$_ChatStoreBaseActionController.startAction( + name: '_ChatStoreBase.resumeScroll'); + try { + return super.resumeScroll(); + } finally { + _$_ChatStoreBaseActionController.endAction(_$actionInfo); + } + } + + @override + String toString() { + return ''' +autoScroll: ${autoScroll} + '''; + } +} From ac3f79915aba34902ab31ffbf7215a791cf6cf8b Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 1 Oct 2021 00:08:43 -0400 Subject: [PATCH 02/15] Add MobX dependencies --- pubspec.lock | 48 +++++++++++++++++++++++++++++++++++++++++++++--- pubspec.yaml | 3 +++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index dba3ea34..e07a6db7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -244,6 +244,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_mobx: + dependency: "direct main" + description: + name: flutter_mobx + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" flutter_secure_storage: dependency: "direct main" description: @@ -415,6 +422,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + mobx: + dependency: "direct main" + description: + name: mobx + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" nested: dependency: transitive description: @@ -519,7 +540,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.0" + version: "6.0.1" pub_semver: dependency: transitive description: @@ -566,14 +587,14 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" source_helper: dependency: transitive description: name: source_helper url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.3.0" source_span: dependency: transitive description: @@ -692,6 +713,27 @@ packages: name: webview_flutter url: "https://pub.dartlang.org" source: hosted + version: "2.1.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.15" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + url: "https://pub.dartlang.org" + source: hosted version: "2.0.14" win32: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 1d824fcb..ffe4f5d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,8 @@ dependencies: intl: ^0.17.0 cached_network_image: ^3.1.0 webview_flutter: ^2.0.13 + mobx: ^2.0.0 + flutter_mobx: ^2.0.0 dev_dependencies: flutter_test: @@ -55,6 +57,7 @@ dev_dependencies: flutter_lints: ^1.0.0 build_runner: ^2.1.1 json_serializable: ^5.0.1 + mobx_codegen: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From b253fcb54b53d6dad20757ecfad02482e4116d17 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 1 Oct 2021 00:10:04 -0400 Subject: [PATCH 03/15] Fix navigation pop rebuild home --- lib/main.dart | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 16873678..2465f386 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:frosty/providers/authentication_provider.dart'; -import 'package:frosty/providers/settings_provider.dart'; import 'package:frosty/screens/home.dart'; -import 'package:frosty/providers/channel_list_provider.dart'; +import 'package:frosty/stores/auth_store.dart'; +import 'package:frosty/stores/channel_list_store.dart'; import 'package:provider/provider.dart'; void main() { @@ -16,27 +15,36 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => AuthenticationProvider()), - ChangeNotifierProxyProvider( - create: (_) => SettingsProvider(), - update: (context, auth, settingsProvider) { - return SettingsProvider(); - }, - ), - ChangeNotifierProxyProvider( - create: (_) => ChannelListProvider(), + Provider(create: (_) => AuthStore()), + ProxyProvider( + create: (_) => ChannelListStore(), update: (context, auth, channelListProvider) { - return ChannelListProvider(id: auth.user?.id); + return ChannelListStore(id: auth.user?.id); }, ), ], - child: MaterialApp( - title: 'Frosty', - theme: ThemeData( - primarySwatch: Colors.blue, - brightness: Brightness.dark, - ), - home: const Home(), + child: Builder( + builder: (context) { + final auth = context.watch(); + return MaterialApp( + title: 'Frosty', + theme: ThemeData( + primarySwatch: Colors.blue, + brightness: Brightness.dark, + ), + home: Scaffold( + body: FutureBuilder( + future: auth.init(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return const Home(); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ); + }, ), ); } From ff4803c7080f918d204737a86f9a96c0a8fbd607 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 1 Oct 2021 00:10:50 -0400 Subject: [PATCH 04/15] Migrate to MobX --- lib/screens/channel_list.dart | 9 +-- lib/screens/home.dart | 101 ++++++++++++++++------------------ lib/screens/settings.dart | 25 +++++---- lib/utility/request.dart | 10 ++-- lib/widgets/channel_card.dart | 7 +-- lib/widgets/chat.dart | 29 +++++++--- 6 files changed, 91 insertions(+), 90 deletions(-) diff --git a/lib/screens/channel_list.dart b/lib/screens/channel_list.dart index 76e844a5..c70d90aa 100644 --- a/lib/screens/channel_list.dart +++ b/lib/screens/channel_list.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:frosty/providers/authentication_provider.dart'; -import 'package:frosty/providers/channel_list_provider.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:frosty/stores/channel_list_store.dart'; import 'package:frosty/widgets/channel_card.dart'; import 'package:provider/provider.dart'; @@ -17,8 +17,9 @@ class _ChannelListState extends State with AutomaticKeepAliveClient @override Widget build(BuildContext context) { super.build(context); - return Consumer2( - builder: (context, auth, viewModel, child) { + final viewModel = context.watch(); + return Observer( + builder: (_) { final channels = viewModel.channels(category: widget.category); return RefreshIndicator( child: ListView.builder( diff --git a/lib/screens/home.dart b/lib/screens/home.dart index b4e30342..3d516938 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:frosty/providers/authentication_provider.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/settings.dart'; -import 'package:frosty/providers/channel_list_provider.dart'; +import 'package:frosty/stores/auth_store.dart'; +import 'package:frosty/stores/channel_list_store.dart'; import 'package:provider/provider.dart'; import 'channel_list.dart'; @@ -10,63 +11,55 @@ class Home extends StatelessWidget { @override Widget build(BuildContext context) { - final auth = context.watch(); + final auth = context.watch(); debugPrint('build home'); - return FutureBuilder( - future: auth.init(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return DefaultTabController( - length: auth.isLoggedIn ? 3 : 2, - child: Scaffold( - appBar: AppBar( - title: const Text('Frosty for Twitch'), - bottom: TabBar( - tabs: [ - const Tab(text: 'Top'), - if (auth.isLoggedIn) const Tab(text: 'Followed'), - const Tab(text: 'Categories'), - ], - ), - actions: [ - IconButton( - icon: const Icon(Icons.settings), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const Settings(); - }, - ), - ); - }, - ) - ], - ), - body: TabBarView( - physics: const NeverScrollableScrollPhysics(), - children: [ - const ChannelList( - category: Category.top, - ), - if (auth.isLoggedIn) - const ChannelList( - category: Category.followed, - ), - const Center( - child: Text('Games'), - ), + return Observer( + builder: (_) { + return DefaultTabController( + length: auth.isLoggedIn ? 3 : 2, + child: Scaffold( + appBar: AppBar( + title: const Text('Frosty for Twitch'), + bottom: TabBar( + tabs: [ + const Tab(text: 'Top'), + if (auth.isLoggedIn) const Tab(text: 'Followed'), + const Tab(text: 'Categories'), ], ), + actions: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const Settings(); + }, + ), + ); + }, + ) + ], ), - ); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } + body: TabBarView( + children: [ + const ChannelList( + category: Category.top, + ), + if (auth.isLoggedIn) + const ChannelList( + category: Category.followed, + ), + const Center( + child: Text('Games'), + ), + ], + ), + ), + ); }, ); } diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index cbbeb19a..9c64ef07 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:frosty/providers/authentication_provider.dart'; -import 'package:frosty/providers/settings_provider.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:frosty/stores/auth_store.dart'; import 'package:provider/provider.dart'; class Settings extends StatelessWidget { @@ -8,20 +8,21 @@ class Settings extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer2( - builder: (context, settings, auth, child) { - return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - ), - body: Center( + final auth = context.watch(); + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + ), + body: Observer( + builder: (_) { + return Center( child: ElevatedButton( child: auth.isLoggedIn ? Text(auth.user!.displayName) : const Text('Login'), onPressed: () => auth.isLoggedIn ? auth.logout() : auth.login(), ), - ), - ); - }, + ); + }, + ), ); } } diff --git a/lib/utility/request.dart b/lib/utility/request.dart index 637cb6fa..16ebf8c9 100644 --- a/lib/utility/request.dart +++ b/lib/utility/request.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; -import 'package:frosty/providers/authentication_provider.dart'; +import 'package:frosty/stores/auth_store.dart'; import 'package:http/http.dart' as http; // A class for general requests. @@ -93,7 +93,7 @@ class Request { /// Returns a map of global Twitch emotes to their URL. static Future?> getEmotesTwitchGlobal() async { final url = Uri.parse('https://api.twitch.tv/helix/chat/emotes/global'); - final response = await http.get(url, headers: AuthenticationProvider.authHeaders); + final response = await http.get(url, headers: AuthBase.authHeaders); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; @@ -113,7 +113,7 @@ class Request { /// Returns a map of a channel's Twitch emotes to their URL. static Future?> getEmotesTwitchChannel({required String id}) async { final url = Uri.parse('https://api.twitch.tv/helix/chat/emotes?broadcaster_id=$id'); - final response = await http.get(url, headers: AuthenticationProvider.authHeaders); + final response = await http.get(url, headers: AuthBase.authHeaders); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; @@ -133,7 +133,7 @@ class Request { /// Returns a map of global Twitch badges to their URL. static Future?> getBadgesTwitchGlobal() async { final url = Uri.parse('https://api.twitch.tv/helix/chat/badges/global'); - final response = await http.get(url, headers: AuthenticationProvider.authHeaders); + final response = await http.get(url, headers: AuthBase.authHeaders); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; @@ -155,7 +155,7 @@ class Request { /// Returns a map of a channel's Twitch badges to their URL. static Future?> getBadgesTwitchChannel({required String id}) async { final url = Uri.parse('https://api.twitch.tv/helix/chat/badges?broadcaster_id=$id'); - final response = await http.get(url, headers: AuthenticationProvider.authHeaders); + final response = await http.get(url, headers: AuthBase.authHeaders); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; diff --git a/lib/widgets/channel_card.dart b/lib/widgets/channel_card.dart index 3eb722d2..7714bd3d 100644 --- a/lib/widgets/channel_card.dart +++ b/lib/widgets/channel_card.dart @@ -2,9 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:frosty/models/channel.dart'; -import 'package:frosty/providers/chat_provider.dart'; import 'package:frosty/screens/video_chat.dart'; -import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; /// A card widget that displays a live channel's thumbnail and details. @@ -23,10 +21,7 @@ class ChannelCard extends StatelessWidget { CupertinoPageRoute( builder: (context) { return Scaffold( - body: ChangeNotifierProvider( - create: (context) => ChatProvider(channelInfo: channelInfo), - child: VideoChat(channelInfo: channelInfo), - ), + body: VideoChat(channelInfo: channelInfo), ); }, ), diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index 582497c0..4b93f633 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -1,20 +1,25 @@ import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/models/channel.dart'; -import 'package:frosty/providers/chat_provider.dart'; -import 'package:provider/provider.dart'; +import 'package:frosty/stores/chat_store.dart'; -class Chat extends StatelessWidget { +class Chat extends StatefulWidget { final Channel channelInfo; const Chat({Key? key, required this.channelInfo}) : super(key: key); + @override + _ChatState createState() => _ChatState(); +} + +class _ChatState extends State { + final viewModel = ChatStore(); + @override Widget build(BuildContext context) { - debugPrint('build'); - final viewModel = context.read(); return FutureBuilder( - future: viewModel.getEmotes(), - builder: (context, snapshot) { + future: viewModel.start(widget.channelInfo), + builder: (_, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return StreamBuilder( stream: viewModel.channel.stream, @@ -34,8 +39,8 @@ class Chat extends StatelessWidget { return viewModel.parseIrcMessage(viewModel.messages[index]); }, ), - Consumer( - builder: (context, viewModel, child) { + Observer( + builder: (_) { return Visibility( visible: !viewModel.autoScroll, child: Container( @@ -60,4 +65,10 @@ class Chat extends StatelessWidget { }, ); } + + @override + void dispose() { + viewModel.channel.sink.close(); + super.dispose(); + } } From 959584417e16c616e80567dbece0a67b6e58d161 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 1 Oct 2021 01:35:01 -0400 Subject: [PATCH 05/15] Update packages --- lib/main.dart | 2 +- pubspec.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 2465f386..c995bf01 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,7 @@ class MyApp extends StatelessWidget { Provider(create: (_) => AuthStore()), ProxyProvider( create: (_) => ChannelListStore(), - update: (context, auth, channelListProvider) { + update: (context, auth, channelListStore) { return ChannelListStore(id: auth.user?.id); }, ), diff --git a/pubspec.lock b/pubspec.lock index e07a6db7..f4807de0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -140,7 +140,7 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.4" clock: dependency: transitive description: @@ -323,7 +323,7 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" graphs: dependency: transitive description: @@ -519,7 +519,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" pool: dependency: transitive description: @@ -699,7 +699,7 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" web_socket_channel: dependency: transitive description: From 5b8ebb65eb8b0bdd64068838f11d28572994af41 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 1 Oct 2021 03:01:45 -0400 Subject: [PATCH 06/15] Renames & minor fixes --- lib/main.dart | 2 +- lib/screens/channel_list.dart | 10 +++++----- lib/screens/home.dart | 2 +- lib/screens/settings.dart | 2 +- lib/stores/channel_list_store.dart | 26 +++++++------------------- lib/stores/chat_store.g.dart | 7 +++++++ lib/widgets/chat.dart | 20 ++++++++++---------- 7 files changed, 32 insertions(+), 37 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c995bf01..7cbc9d32 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,7 +25,7 @@ class MyApp extends StatelessWidget { ], child: Builder( builder: (context) { - final auth = context.watch(); + final auth = context.read(); return MaterialApp( title: 'Frosty', theme: ThemeData( diff --git a/lib/screens/channel_list.dart b/lib/screens/channel_list.dart index c70d90aa..4324400a 100644 --- a/lib/screens/channel_list.dart +++ b/lib/screens/channel_list.dart @@ -17,22 +17,22 @@ class _ChannelListState extends State with AutomaticKeepAliveClient @override Widget build(BuildContext context) { super.build(context); - final viewModel = context.watch(); + final channelListStore = context.read(); return Observer( builder: (_) { - final channels = viewModel.channels(category: widget.category); + final channels = channelListStore.channels(category: widget.category); return RefreshIndicator( child: ListView.builder( itemCount: channels.length, padding: const EdgeInsets.all(5.0), itemBuilder: (context, index) { - if (index > channels.length / 2 && viewModel.isLoading == false && viewModel.currentCursor(category: widget.category) != null) { - viewModel.getMoreChannels(category: widget.category); + if (index > channels.length / 2 && channelListStore.isLoading == false && channelListStore.currentCursor(category: widget.category) != null) { + channelListStore.getMoreChannels(category: widget.category); } return ChannelCard(channelInfo: channels[index]); }, ), - onRefresh: () => viewModel.update(category: widget.category), + onRefresh: () => channelListStore.update(category: widget.category), ); }, ); diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 3d516938..adb4ae86 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -11,7 +11,7 @@ class Home extends StatelessWidget { @override Widget build(BuildContext context) { - final auth = context.watch(); + final auth = context.read(); debugPrint('build home'); return Observer( diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 9c64ef07..1694d144 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -8,7 +8,7 @@ class Settings extends StatelessWidget { @override Widget build(BuildContext context) { - final auth = context.watch(); + final auth = context.read(); return Scaffold( appBar: AppBar( title: const Text('Settings'), diff --git a/lib/stores/channel_list_store.dart b/lib/stores/channel_list_store.dart index 15452a3d..fc8f251c 100644 --- a/lib/stores/channel_list_store.dart +++ b/lib/stores/channel_list_store.dart @@ -47,7 +47,7 @@ abstract class _ChannelListBase with Store { } } - List channels({required Category category}) { + ObservableList channels({required Category category}) { switch (category) { case Category.top: return topChannels; @@ -80,10 +80,7 @@ abstract class _ChannelListBase with Store { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - final result = { - 'channels': data.map((channel) => Channel.fromJson(channel)).toList(), - 'cursor': decoded['pagination']['cursor'] - }; + final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; topChannels = ObservableList.of(result['channels']); topChannelsCurrentCursor = result['cursor']; @@ -100,12 +97,10 @@ abstract class _ChannelListBase with Store { switch (category) { case Category.top: - url = Uri.parse( - 'https://api.twitch.tv/helix/streams?first=10&after=$topChannelsCurrentCursor'); + url = Uri.parse('https://api.twitch.tv/helix/streams?first=10&after=$topChannelsCurrentCursor'); break; case Category.followed: - url = Uri.parse( - 'https://api.twitch.tv/helix/streams/followed?user_id=$id&first=10&after=$followedChannelsCurrentCursor'); + url = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id&first=10&after=$followedChannelsCurrentCursor'); break; } @@ -116,10 +111,7 @@ abstract class _ChannelListBase with Store { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - final result = { - 'channels': data.map((channel) => Channel.fromJson(channel)).toList(), - 'cursor': decoded['pagination']['cursor'] - }; + final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; switch (category) { case Category.top: @@ -141,8 +133,7 @@ abstract class _ChannelListBase with Store { @action Future updateFollowedChannels() async { - final url = Uri.parse( - 'https://api.twitch.tv/helix/streams/followed?first=10&user_id=$id'); + final url = Uri.parse('https://api.twitch.tv/helix/streams/followed?first=10&user_id=$id'); final headers = {'Authorization': 'Bearer $token', 'Client-Id': clientId}; final response = await http.get(url, headers: headers); @@ -151,10 +142,7 @@ abstract class _ChannelListBase with Store { final decoded = jsonDecode(response.body); final data = decoded['data'] as List; - final result = { - 'channels': data.map((channel) => Channel.fromJson(channel)).toList(), - 'cursor': decoded['pagination']['cursor'] - }; + final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; followedChannels = ObservableList.of(result['channels']); followedChannelsCurrentCursor = result['cursor']; diff --git a/lib/stores/chat_store.g.dart b/lib/stores/chat_store.g.dart index 77fd94fd..1ceb996f 100644 --- a/lib/stores/chat_store.g.dart +++ b/lib/stores/chat_store.g.dart @@ -24,6 +24,13 @@ mixin _$ChatStore on _ChatStoreBase, Store { }); } + final _$getEmotesAsyncAction = AsyncAction('_ChatStoreBase.getEmotes'); + + @override + Future getEmotes() { + return _$getEmotesAsyncAction.run(() => super.getEmotes()); + } + final _$_ChatStoreBaseActionController = ActionController(name: '_ChatStoreBase'); diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index 4b93f633..de7e5167 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -13,18 +13,18 @@ class Chat extends StatefulWidget { } class _ChatState extends State { - final viewModel = ChatStore(); + late ChatStore chatStore = ChatStore(channelInfo: widget.channelInfo); @override Widget build(BuildContext context) { return FutureBuilder( - future: viewModel.start(widget.channelInfo), + future: chatStore.getEmotes(), builder: (_, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return StreamBuilder( - stream: viewModel.channel.stream, + stream: chatStore.channel.stream, builder: (context, snapshot) { - viewModel.handleWebsocketData(snapshot.data); + chatStore.handleWebsocketData(snapshot.data); return Stack( alignment: AlignmentDirectional.bottomCenter, children: [ @@ -32,22 +32,22 @@ class _ChatState extends State { addAutomaticKeepAlives: false, addRepaintBoundaries: false, physics: const ClampingScrollPhysics(), - itemCount: viewModel.messages.length, - controller: viewModel.scrollController, + itemCount: chatStore.messages.length, + controller: chatStore.scrollController, padding: const EdgeInsets.all(5.0), itemBuilder: (context, index) { - return viewModel.parseIrcMessage(viewModel.messages[index]); + return chatStore.parseIrcMessage(chatStore.messages[index]); }, ), Observer( builder: (_) { return Visibility( - visible: !viewModel.autoScroll, + visible: !chatStore.autoScroll, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4.0), width: double.infinity, child: ElevatedButton( - onPressed: () => viewModel.resumeScroll(), + onPressed: () => chatStore.resumeScroll(), child: const Text('Resume Scroll'), ), ), @@ -68,7 +68,7 @@ class _ChatState extends State { @override void dispose() { - viewModel.channel.sink.close(); + chatStore.channel.sink.close(); super.dispose(); } } From a5acd74351266f606f507ec4091e7cd209045d4c Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Fri, 1 Oct 2021 03:02:14 -0400 Subject: [PATCH 07/15] Revert chat changes --- lib/stores/chat_store.dart | 43 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/stores/chat_store.dart b/lib/stores/chat_store.dart index 73f958d1..dfea7d11 100644 --- a/lib/stores/chat_store.dart +++ b/lib/stores/chat_store.dart @@ -18,6 +18,7 @@ abstract class _ChatStoreBase with Store { List messages = []; + final Channel channelInfo; final channel = WebSocketChannel.connect(Uri.parse('wss://irc-ws.chat.twitch.tv:443')); final _assetToUrl = {}; @@ -25,25 +26,7 @@ abstract class _ChatStoreBase with Store { final scrollController = ScrollController(); - Future start(Channel channelInfo) async { - final assets = [ - await Request.getEmotesBTTVGlobal(), - await Request.getEmotesBTTVChannel(id: channelInfo.userId), - await Request.getEmotesFFZGlobal(), - await Request.getEmotesFFZChannel(id: channelInfo.userId), - await Request.getEmotesTwitchGlobal(), - await Request.getEmotesTwitchChannel(id: channelInfo.userId), - await Request.getBadgesTwitchGlobal(), - await Request.getBadgesTwitchChannel(id: channelInfo.userId), - await Request.getEmotes7TVGlobal(), - await Request.getEmotes7TVChannel(user: channelInfo.userLogin) - ]; - - for (final map in assets) { - if (map != null) { - _assetToUrl.addAll(map); - } - } + _ChatStoreBase({required this.channelInfo}) { final commands = [ 'PASS oauth:${AuthBase.token}', 'NICK justinfan888', @@ -67,6 +50,28 @@ abstract class _ChatStoreBase with Store { }); } + @action + Future getEmotes() async { + final assets = [ + await Request.getEmotesBTTVGlobal(), + await Request.getEmotesBTTVChannel(id: channelInfo.userId), + await Request.getEmotesFFZGlobal(), + await Request.getEmotesFFZChannel(id: channelInfo.userId), + await Request.getEmotesTwitchGlobal(), + await Request.getEmotesTwitchChannel(id: channelInfo.userId), + await Request.getBadgesTwitchGlobal(), + await Request.getBadgesTwitchChannel(id: channelInfo.userId), + await Request.getEmotes7TVGlobal(), + await Request.getEmotes7TVChannel(user: channelInfo.userLogin) + ]; + + for (final map in assets) { + if (map != null) { + _assetToUrl.addAll(map); + } + } + } + @action void handleWebsocketData(Object? data) { for (final message in data.toString().split('\r\n')) { From 8c2038f92d4bca1ecfd6cc9053f64b54935a85b6 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 18:08:51 -0400 Subject: [PATCH 08/15] Refactor requests to their own dedicated API files --- lib/api/bttv_api.dart | 49 +++++++++ lib/api/ffz_api.dart | 46 +++++++++ lib/api/seventv_api.dart | 45 ++++++++ lib/api/twitch_api.dart | 179 ++++++++++++++++++++++++++++++++ lib/utility/request.dart | 214 --------------------------------------- 5 files changed, 319 insertions(+), 214 deletions(-) create mode 100644 lib/api/bttv_api.dart create mode 100644 lib/api/ffz_api.dart create mode 100644 lib/api/seventv_api.dart create mode 100644 lib/api/twitch_api.dart delete mode 100644 lib/utility/request.dart diff --git a/lib/api/bttv_api.dart b/lib/api/bttv_api.dart new file mode 100644 index 00000000..7dab6c23 --- /dev/null +++ b/lib/api/bttv_api.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:frosty/models/emotes.dart'; +import 'package:http/http.dart' as http; + +class BTTV { + /// Returns a map of global BTTV emotes to their URL. + static Future?> getEmotesGlobal() async { + final url = Uri.parse('https://api.betterttv.net/3/cached/emotes/global'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body) as List; + final List emotes = decoded.map((emote) => EmoteBTTVGlobal.fromJson(emote)).toList(); + + final emoteToUrl = {}; + for (final emote in emotes) { + emoteToUrl[emote.code] = 'https://cdn.betterttv.net/emote/${emote.id}/3x'; + } + + return emoteToUrl; + } else { + debugPrint('Failed to get global BTTV emotes. Error code: ${response.statusCode}'); + } + } + + /// Returns a map of a channel's BTTV emotes to their URL. + static Future?> getEmotesChannel({required String id}) async { + final url = Uri.parse('https://api.betterttv.net/3/cached/users/twitch/$id'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + final result = EmoteBTTVChannel.fromJson(decoded); + + final emoteToUrl = {}; + for (final emote in result.channelEmotes) { + emoteToUrl[emote.code] = 'https://cdn.betterttv.net/emote/${emote.id}/3x'; + } + for (final emote in result.sharedEmotes) { + emoteToUrl[emote.code] = 'https://cdn.betterttv.net/emote/${emote.id}/3x'; + } + + return emoteToUrl; + } else { + debugPrint('Failed to get BTTV emotes for id: $id. Error code: ${response.statusCode}'); + } + } +} diff --git a/lib/api/ffz_api.dart b/lib/api/ffz_api.dart new file mode 100644 index 00000000..b6e9d677 --- /dev/null +++ b/lib/api/ffz_api.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:frosty/models/emotes.dart'; +import 'package:http/http.dart' as http; + +class FFZ { + /// Returns a map of global FFZ emotes to their URL. + static Future?> getEmotesGlobal() async { + final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/emotes/global'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body) as List; + final List emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); + + final emoteToUrl = {}; + for (final emote in emotes) { + emoteToUrl[emote.code] = emote.images.url4x ?? emote.images.url1x; + } + + return emoteToUrl; + } else { + debugPrint('Failed to get global FFZ emotes. Error code: ${response.statusCode}'); + } + } + + /// Returns a map of a channel's FFZ emotes to their URL. + static Future?> getEmotesChannel({required String id}) async { + final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/users/twitch/$id'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body) as List; + final List emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); + + final emoteToUrl = {}; + for (final emote in emotes) { + emoteToUrl[emote.code] = emote.images.url4x ?? emote.images.url1x; + } + + return emoteToUrl; + } else { + debugPrint('Failed to get FFZ emotes for id: $id. Error code: ${response.statusCode}'); + } + } +} diff --git a/lib/api/seventv_api.dart b/lib/api/seventv_api.dart new file mode 100644 index 00000000..13041450 --- /dev/null +++ b/lib/api/seventv_api.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:frosty/models/emotes.dart'; +import 'package:http/http.dart' as http; + +// A class for general requests. +class SevenTV { + /// Returns a map of global 7TV emotes to their URL. + static Future?> getEmotesGlobal() async { + final url = Uri.parse('https://api.7tv.app/v2/emotes/global'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body) as List; + final List emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); + + final emoteToUrl = {}; + for (final emote in emotes) { + emoteToUrl[emote.name] = emote.urls[3][1]; + } + return emoteToUrl; + } else { + debugPrint('Failed to get global 7TV emotes. Error code: ${response.statusCode}'); + } + } + + /// Returns a map of a channel's 7TV emotes to their URL. + static Future?> getEmotesChannel({required String user}) async { + final url = Uri.parse('https://api.7tv.app/v2/users/$user/emotes'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body) as List; + final List emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); + + final emoteToUrl = {}; + for (final emote in emotes) { + emoteToUrl[emote.name] = emote.urls[3][1]; + } + return emoteToUrl; + } else { + debugPrint('Failed to get channel 7TV emotes. Error code: ${response.statusCode}'); + } + } +} diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart new file mode 100644 index 00000000..2cc7427b --- /dev/null +++ b/lib/api/twitch_api.dart @@ -0,0 +1,179 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:frosty/constants.dart'; +import 'package:frosty/models/badges.dart'; +import 'package:frosty/models/channel.dart'; +import 'package:frosty/models/emotes.dart'; +import 'package:frosty/models/user.dart'; +import 'package:http/http.dart' as http; + +class Twitch { + static Future?> getEmotesGlobal({required Map? headers}) async { + final url = Uri.parse('https://api.twitch.tv/helix/chat/emotes/global'); + final response = await http.get(url, headers: headers); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body)['data'] as List; + final List emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); + + final emoteToUrl = {}; + for (final emote in emotes) { + emoteToUrl[emote.name] = 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0'; + } + + return emoteToUrl; + } else { + debugPrint('Failed to get global Twitch emotes. Error code: ${response.statusCode}'); + } + } + + /// Returns a map of a channel's Twitch emotes to their URL. + static Future?> getEmotesChannel({required String id, required Map? headers}) async { + final url = Uri.parse('https://api.twitch.tv/helix/chat/emotes?broadcaster_id=$id'); + final response = await http.get(url, headers: headers); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body)['data'] as List; + final List emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); + + final emoteToUrl = {}; + for (final emote in emotes) { + emoteToUrl[emote.name] = 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0'; + } + + return emoteToUrl; + } else { + debugPrint('Failed to get Twitch emotes for id: $id. Error code: ${response.statusCode}'); + } + } + + /// Returns a map of global Twitch badges to their URL. + static Future?> getBadgesGlobal({required Map? headers}) async { + final url = Uri.parse('https://api.twitch.tv/helix/chat/badges/global'); + final response = await http.get(url, headers: headers); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body)['data'] as List; + final List badges = decoded.map((emote) => BadgesTwitch.fromJson(emote)).toList(); + + final badgeToUrl = {}; + for (final badge in badges) { + for (final badgeVersion in badge.versions) { + badgeToUrl['${badge.setId}/${badgeVersion.id}'] = badgeVersion.imageUrl4x; + } + } + + return badgeToUrl; + } else { + debugPrint('Failed to get global Twitch badges. Error code: ${response.statusCode}'); + } + } + + /// Returns a map of a channel's Twitch badges to their URL. + static Future?> getBadgesChannel({required String id, required Map? headers}) async { + final url = Uri.parse('https://api.twitch.tv/helix/chat/badges?broadcaster_id=$id'); + final response = await http.get(url, headers: headers); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body)['data'] as List; + final List badges = decoded.map((emote) => BadgesTwitch.fromJson(emote)).toList(); + + final badgeToUrl = {}; + for (final badge in badges) { + for (final badgeVersion in badge.versions) { + badgeToUrl['${badge.setId}/${badgeVersion.id}'] = badgeVersion.imageUrl4x; + } + } + + return badgeToUrl; + } else { + debugPrint('Failed to get Twitch badges for id: $id. Error code: ${response.statusCode}'); + } + } + + /// Returns the user's info given their token (headers) + static Future getUserInfo({required Map? headers}) async { + final response = await http.get(Uri.parse('https://api.twitch.tv/helix/users'), headers: headers); + final userData = jsonDecode(response.body)['data'] as List; + + return UserTwitch.fromJson(userData.first); + } + + /// Returns a token for an anonymous user. + static Future getDefaultToken() async { + debugPrint('Getting default token...'); + + final url = Uri( + scheme: 'https', + host: 'id.twitch.tv', + path: '/oauth2/token', + queryParameters: { + 'client_id': clientId, + 'client_secret': secret, + 'grant_type': 'client_credentials', + }, + ); + + final response = await http.post(url); + final defaultToken = jsonDecode(response.body)['access_token']; + + return defaultToken; + } + + /// Returns the validity of the given token + static Future validateToken({required String token}) async { + debugPrint('Validating token...'); + final response = await http.get(Uri.parse('https://id.twitch.tv/oauth2/validate'), headers: {'Authorization': 'Bearer $token'}); + if (response.statusCode == 200) { + debugPrint('Token validated!'); + return true; + } + + debugPrint('Token invalidated :('); + return false; + } + + /// Returns a map with top 10 streamers and a cursor for further requests. + static Future?> getTopChannels({required Map? headers, required String? cursor}) async { + final Uri uri; + + if (cursor == null) { + uri = Uri.parse('https://api.twitch.tv/helix/streams?first=10'); + } else { + uri = Uri.parse('https://api.twitch.tv/helix/streams?first=10&after=$cursor'); + } + + final response = await http.get(uri, headers: headers); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + final data = decoded['data'] as List; + + return {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; + } else { + debugPrint('Failed to update top channels'); + } + } + + /// Returns a map with the given user's top 10 followed streamers and a cursor for further requests. + static Future?> getFollowedChannels({required String id, required Map? headers, required String? cursor}) async { + final Uri uri; + + if (cursor == null) { + uri = Uri.parse('https://api.twitch.tv/helix/streams/followed?first=10&user_id=$id'); + } else { + uri = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id&first=10&after=$cursor'); + } + + final response = await http.get(uri, headers: headers); + + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + final data = decoded['data'] as List; + + return {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; + } else { + debugPrint('Failed to update followed channels'); + } + } +} diff --git a/lib/utility/request.dart b/lib/utility/request.dart deleted file mode 100644 index 16ebf8c9..00000000 --- a/lib/utility/request.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:frosty/models/badges.dart'; -import 'package:frosty/models/emotes.dart'; -import 'package:frosty/stores/auth_store.dart'; -import 'package:http/http.dart' as http; - -// A class for general requests. -class Request { - /// Returns a map of global BTTV emotes to their URL. - static Future?> getEmotesBTTVGlobal() async { - final url = Uri.parse('https://api.betterttv.net/3/cached/emotes/global'); - final response = await http.get(url); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => EmoteBTTVGlobal.fromJson(emote)).toList(); - - final emoteToUrl = {}; - for (final emote in emotes) { - emoteToUrl[emote.code] = 'https://cdn.betterttv.net/emote/${emote.id}/3x'; - } - - return emoteToUrl; - } else { - debugPrint('Failed to get global BTTV emotes. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of a channel's BTTV emotes to their URL. - static Future?> getEmotesBTTVChannel({required String id}) async { - final url = Uri.parse('https://api.betterttv.net/3/cached/users/twitch/$id'); - final response = await http.get(url); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body); - final result = EmoteBTTVChannel.fromJson(decoded); - - final emoteToUrl = {}; - for (final emote in result.channelEmotes) { - emoteToUrl[emote.code] = 'https://cdn.betterttv.net/emote/${emote.id}/3x'; - } - for (final emote in result.sharedEmotes) { - emoteToUrl[emote.code] = 'https://cdn.betterttv.net/emote/${emote.id}/3x'; - } - - return emoteToUrl; - } else { - debugPrint('Failed to get BTTV emotes for id: $id. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of global FFZ emotes to their URL. - static Future?> getEmotesFFZGlobal() async { - final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/emotes/global'); - final response = await http.get(url); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); - - final emoteToUrl = {}; - for (final emote in emotes) { - emoteToUrl[emote.code] = emote.images.url4x ?? emote.images.url1x; - } - - return emoteToUrl; - } else { - debugPrint('Failed to get global FFZ emotes. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of a channel's FFZ emotes to their URL. - static Future?> getEmotesFFZChannel({required String id}) async { - final url = Uri.parse('https://api.betterttv.net/3/cached/frankerfacez/users/twitch/$id'); - final response = await http.get(url); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => EmoteFFZ.fromJson(emote)).toList(); - - final emoteToUrl = {}; - for (final emote in emotes) { - emoteToUrl[emote.code] = emote.images.url4x ?? emote.images.url1x; - } - - return emoteToUrl; - } else { - debugPrint('Failed to get FFZ emotes for id: $id. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of global Twitch emotes to their URL. - static Future?> getEmotesTwitchGlobal() async { - final url = Uri.parse('https://api.twitch.tv/helix/chat/emotes/global'); - final response = await http.get(url, headers: AuthBase.authHeaders); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body)['data'] as List; - final List emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); - - final emoteToUrl = {}; - for (final emote in emotes) { - emoteToUrl[emote.name] = 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0'; - } - - return emoteToUrl; - } else { - debugPrint('Failed to get global Twitch emotes. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of a channel's Twitch emotes to their URL. - static Future?> getEmotesTwitchChannel({required String id}) async { - final url = Uri.parse('https://api.twitch.tv/helix/chat/emotes?broadcaster_id=$id'); - final response = await http.get(url, headers: AuthBase.authHeaders); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body)['data'] as List; - final List emotes = decoded.map((emote) => EmoteTwitch.fromJson(emote)).toList(); - - final emoteToUrl = {}; - for (final emote in emotes) { - emoteToUrl[emote.name] = 'https://static-cdn.jtvnw.net/emoticons/v2/${emote.id}/default/dark/3.0'; - } - - return emoteToUrl; - } else { - debugPrint('Failed to get Twitch emotes for id: $id. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of global Twitch badges to their URL. - static Future?> getBadgesTwitchGlobal() async { - final url = Uri.parse('https://api.twitch.tv/helix/chat/badges/global'); - final response = await http.get(url, headers: AuthBase.authHeaders); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body)['data'] as List; - final List badges = decoded.map((emote) => BadgesTwitch.fromJson(emote)).toList(); - - final badgeToUrl = {}; - for (final badge in badges) { - for (final badgeVersion in badge.versions) { - badgeToUrl['${badge.setId}/${badgeVersion.id}'] = badgeVersion.imageUrl4x; - } - } - - return badgeToUrl; - } else { - debugPrint('Failed to get global Twitch badges. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of a channel's Twitch badges to their URL. - static Future?> getBadgesTwitchChannel({required String id}) async { - final url = Uri.parse('https://api.twitch.tv/helix/chat/badges?broadcaster_id=$id'); - final response = await http.get(url, headers: AuthBase.authHeaders); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body)['data'] as List; - final List badges = decoded.map((emote) => BadgesTwitch.fromJson(emote)).toList(); - - final badgeToUrl = {}; - for (final badge in badges) { - for (final badgeVersion in badge.versions) { - badgeToUrl['${badge.setId}/${badgeVersion.id}'] = badgeVersion.imageUrl4x; - } - } - - return badgeToUrl; - } else { - debugPrint('Failed to get Twitch badges for id: $id. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of global 7TV emotes to their URL. - static Future?> getEmotes7TVGlobal() async { - final url = Uri.parse('https://api.7tv.app/v2/emotes/global'); - final response = await http.get(url); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); - - final emoteToUrl = {}; - for (final emote in emotes) { - emoteToUrl[emote.name] = emote.urls[3][1]; - } - return emoteToUrl; - } else { - debugPrint('Failed to get global 7TV emotes. Error code: ${response.statusCode}'); - } - } - - /// Returns a map of a channel's 7TV emotes to their URL. - static Future?> getEmotes7TVChannel({required String user}) async { - final url = Uri.parse('https://api.7tv.app/v2/users/$user/emotes'); - final response = await http.get(url); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body) as List; - final List emotes = decoded.map((emote) => Emote7TV.fromJson(emote)).toList(); - - final emoteToUrl = {}; - for (final emote in emotes) { - emoteToUrl[emote.name] = emote.urls[3][1]; - } - return emoteToUrl; - } else { - debugPrint('Failed to get channel 7TV emotes. Error code: ${response.statusCode}'); - } - } -} From 3515418375643aa07f5cfc603430f554c36f7a9e Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 22:05:55 -0400 Subject: [PATCH 09/15] Remove provider and add get_it, updates --- lib/models/emotes.dart | 2 -- pubspec.lock | 33 +++++++++++++-------------------- pubspec.yaml | 2 +- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/lib/models/emotes.dart b/lib/models/emotes.dart index d440e12c..967fb883 100644 --- a/lib/models/emotes.dart +++ b/lib/models/emotes.dart @@ -2,8 +2,6 @@ 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/pubspec.lock b/pubspec.lock index f4807de0..80cfc3e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "26.0.0" + version: "27.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.4.0" args: dependency: transitive description: @@ -42,7 +42,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" build_config: dependency: transitive description: @@ -56,7 +56,7 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" build_resolvers: dependency: transitive description: @@ -70,14 +70,14 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.4" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.1.0" + version: "7.2.2" built_collection: dependency: transitive description: @@ -317,6 +317,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + get_it: + dependency: "direct main" + description: + name: get_it + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.0" glob: dependency: transitive description: @@ -436,13 +443,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.3" - nested: - dependency: transitive - description: - name: nested - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" octo_image: dependency: transitive description: @@ -534,13 +534,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.3" - provider: - dependency: "direct main" - description: - name: provider - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.1" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ffe4f5d3..49509f0d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,6 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 http: ^0.13.3 - provider: ^6.0.0 flutter_secure_storage: ^5.0.0-beta.5 flutter_web_auth: ^0.3.1 intl: ^0.17.0 @@ -44,6 +43,7 @@ dependencies: webview_flutter: ^2.0.13 mobx: ^2.0.0 flutter_mobx: ^2.0.0 + get_it: ^7.2.0 dev_dependencies: flutter_test: From 9b02b2b4c48dd6ae3080843ceaaf3690381f5f88 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 22:12:22 -0400 Subject: [PATCH 10/15] Refactor views to use get_it --- lib/main.dart | 50 +++++++++++++---------------------- lib/screens/channel_list.dart | 19 ++++++------- lib/screens/home.dart | 16 ++++++----- lib/screens/settings.dart | 5 ++-- lib/widgets/chat.dart | 6 +++-- 5 files changed, 45 insertions(+), 51 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7cbc9d32..2bb657b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:frosty/screens/home.dart'; import 'package:frosty/stores/auth_store.dart'; -import 'package:frosty/stores/channel_list_store.dart'; -import 'package:provider/provider.dart'; +import 'package:get_it/get_it.dart'; void main() { + GetIt.I.registerSingleton(AuthStore()); + runApp(const MyApp()); } @@ -13,38 +14,23 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiProvider( - providers: [ - Provider(create: (_) => AuthStore()), - ProxyProvider( - create: (_) => ChannelListStore(), - update: (context, auth, channelListStore) { - return ChannelListStore(id: auth.user?.id); + final auth = GetIt.I(); + return MaterialApp( + title: 'Frosty', + theme: ThemeData( + primarySwatch: Colors.blue, + brightness: Brightness.dark, + ), + home: Scaffold( + body: FutureBuilder( + future: auth.init(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return const Home(); + } + return const Center(child: CircularProgressIndicator()); }, ), - ], - child: Builder( - builder: (context) { - final auth = context.read(); - return MaterialApp( - title: 'Frosty', - theme: ThemeData( - primarySwatch: Colors.blue, - brightness: Brightness.dark, - ), - home: Scaffold( - body: FutureBuilder( - future: auth.init(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return const Home(); - } - return const Center(child: CircularProgressIndicator()); - }, - ), - ), - ); - }, ), ); } diff --git a/lib/screens/channel_list.dart b/lib/screens/channel_list.dart index 4324400a..97aa2c5a 100644 --- a/lib/screens/channel_list.dart +++ b/lib/screens/channel_list.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/stores/channel_list_store.dart'; import 'package:frosty/widgets/channel_card.dart'; -import 'package:provider/provider.dart'; class ChannelList extends StatefulWidget { - final Category category; + final ChannelCategory category; + final ChannelListStore channelListStore; - const ChannelList({Key? key, required this.category}) : super(key: key); + const ChannelList({Key? key, required this.category, required this.channelListStore}) : super(key: key); @override _ChannelListState createState() => _ChannelListState(); @@ -16,23 +16,24 @@ class ChannelList extends StatefulWidget { class _ChannelListState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { + debugPrint('build channel list'); super.build(context); - final channelListStore = context.read(); + return Observer( builder: (_) { - final channels = channelListStore.channels(category: widget.category); + final channels = widget.channelListStore.channels(category: widget.category); return RefreshIndicator( child: ListView.builder( itemCount: channels.length, padding: const EdgeInsets.all(5.0), itemBuilder: (context, index) { - if (index > channels.length / 2 && channelListStore.isLoading == false && channelListStore.currentCursor(category: widget.category) != null) { - channelListStore.getMoreChannels(category: widget.category); + if (index > channels.length / 2 && widget.channelListStore.hasMore(category: widget.category)) { + widget.channelListStore.getChannels(category: widget.category); } - return ChannelCard(channelInfo: channels[index]); + return ChannelCard(channelInfo: channels.elementAt(index)); }, ), - onRefresh: () => channelListStore.update(category: widget.category), + onRefresh: () => widget.channelListStore.refresh(category: widget.category), ); }, ); diff --git a/lib/screens/home.dart b/lib/screens/home.dart index adb4ae86..628345d1 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -3,7 +3,7 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/screens/settings.dart'; import 'package:frosty/stores/auth_store.dart'; import 'package:frosty/stores/channel_list_store.dart'; -import 'package:provider/provider.dart'; +import 'package:get_it/get_it.dart'; import 'channel_list.dart'; class Home extends StatelessWidget { @@ -11,11 +11,13 @@ class Home extends StatelessWidget { @override Widget build(BuildContext context) { - final auth = context.read(); + final auth = GetIt.I(); debugPrint('build home'); return Observer( builder: (_) { + final channelListStore = ChannelListStore(auth: auth); + debugPrint('rebuild tab controller'); return DefaultTabController( length: auth.isLoggedIn ? 3 : 2, child: Scaffold( @@ -46,12 +48,14 @@ class Home extends StatelessWidget { ), body: TabBarView( children: [ - const ChannelList( - category: Category.top, + ChannelList( + category: ChannelCategory.top, + channelListStore: channelListStore, ), if (auth.isLoggedIn) - const ChannelList( - category: Category.followed, + ChannelList( + category: ChannelCategory.followed, + channelListStore: channelListStore, ), const Center( child: Text('Games'), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 1694d144..d4f65580 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/stores/auth_store.dart'; -import 'package:provider/provider.dart'; +import 'package:get_it/get_it.dart'; class Settings extends StatelessWidget { const Settings({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final auth = context.read(); + debugPrint('settings'); + final auth = GetIt.I(); return Scaffold( appBar: AppBar( title: const Text('Settings'), diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index de7e5167..65cc7562 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:frosty/models/channel.dart'; +import 'package:frosty/stores/auth_store.dart'; import 'package:frosty/stores/chat_store.dart'; +import 'package:get_it/get_it.dart'; class Chat extends StatefulWidget { final Channel channelInfo; @@ -13,12 +15,12 @@ class Chat extends StatefulWidget { } class _ChatState extends State { - late ChatStore chatStore = ChatStore(channelInfo: widget.channelInfo); + late final ChatStore chatStore = ChatStore(auth: GetIt.I(), channelInfo: widget.channelInfo); @override Widget build(BuildContext context) { return FutureBuilder( - future: chatStore.getEmotes(), + future: chatStore.getAssets(), builder: (_, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return StreamBuilder( From 14b7aa9662157f0c2ca9757f4548c91c8a1084e0 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 22:13:36 -0400 Subject: [PATCH 11/15] Refactor and streamline auth store with comments --- lib/stores/auth_store.dart | 145 +++++++++++++++-------------------- lib/stores/auth_store.g.dart | 80 ++++++++++++++++++- 2 files changed, 139 insertions(+), 86 deletions(-) diff --git a/lib/stores/auth_store.dart b/lib/stores/auth_store.dart index d114d713..80792912 100644 --- a/lib/stores/auth_store.dart +++ b/lib/stores/auth_store.dart @@ -1,114 +1,86 @@ +import 'package:frosty/api/twitch_api.dart'; import 'package:mobx/mobx.dart'; -import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:frosty/constants.dart'; import 'package:frosty/models/user.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:http/http.dart' as http; import 'package:flutter_web_auth/flutter_web_auth.dart'; part 'auth_store.g.dart'; -class AuthStore = AuthBase with _$AuthStore; - -abstract class AuthBase with Store { - static String? _token; - static String? get token => _token; - - static Map? authHeaders; +class AuthStore = _AuthBase with _$AuthStore; +abstract class _AuthBase with Store { + // Secure storage to store tokens final _storage = const FlutterSecureStorage(); - final _secret = secret; - final _clientId = clientId; + // The current token + @observable + String? _token; + String? get token => _token; + + // Whether the user is logged in or not @observable var _isLoggedIn = false; - bool get isLoggedIn => _isLoggedIn; + // Whether the token is valid or not + @observable var _tokenIsValid = false; - UserTwitch? user; + // The current user's info + @observable + UserTwitch? _user; + UserTwitch? get user => _user; + + // Authentication headers for Twitch API requests + @computed + Map get headersTwitch => {'Authorization': 'Bearer $_token', 'Client-Id': clientId}; /// Initialize by retrieving a token if it does not already exist. + @action Future init() async { // Read and set the currently stored user token, if any. _token = await _storage.read(key: 'USER_TOKEN'); // If the token does not exist, get the default token. // Otherwise, log in and get the user info. - if (token != null) { - _isLoggedIn = true; - authHeaders = {'Authorization': 'Bearer $token', 'Client-Id': _clientId}; - await _getUserInfo(); + if (_token == null) { + // Retrieve the currently stored default token if it exists + _token = await _storage.read(key: 'DEFAULT_TOKEN'); + // If the token does not exist, get a new token and store it + if (_token == null) { + _token = await Twitch.getDefaultToken(); + await _storage.write(key: 'DEFAULT_TOKEN', value: _token); + } } else { - await _getDefaultToken(); + _isLoggedIn = true; + _user = await Twitch.getUserInfo(headers: headersTwitch); } - // Set the auth headers for future requests and validate the token. - authHeaders = {'Authorization': 'Bearer $token', 'Client-Id': _clientId}; - await _validateToken(); + // Validate the token + _tokenIsValid = await Twitch.validateToken(token: _token!); - debugPrint('Token is valid: $_tokenIsValid'); - - debugPrint('Created auth provider'); - } - - /// Returns a token for an anonymous user. - Future _getDefaultToken() async { - debugPrint('Getting default token...'); - _token = await _storage.read(key: 'DEFAULT_TOKEN'); - - if (token != null) return; - - final url = Uri( - scheme: 'https', - host: 'id.twitch.tv', - path: '/oauth2/token', - queryParameters: { - 'client_id': _clientId, - 'client_secret': _secret, - 'grant_type': 'client_credentials', - }, - ); - - final response = await http.post(url); - final defaultToken = jsonDecode(response.body)['access_token']; - - await _storage.write(key: 'DEFAULT_TOKEN', value: defaultToken); - - _token = defaultToken; - } - - Future _validateToken() async { - debugPrint('Validating token...'); - final response = await http.get(Uri.parse('https://id.twitch.tv/oauth2/validate'), headers: {'Authorization': 'Bearer $token'}); - if (response.statusCode == 200) { - debugPrint('Token validated!'); - _tokenIsValid = true; + // If the token is invalid, logout + if (!_tokenIsValid) { + logout(); return; } - debugPrint('Token invalidated :('); - _tokenIsValid = false; - - // notifyListeners(); - } - - Future _getUserInfo() async { - final response = await http.get(Uri.parse('https://api.twitch.tv/helix/users'), headers: authHeaders); - final userData = jsonDecode(response.body)['data'] as List; - - user = UserTwitch.fromJson(userData.first); + debugPrint('Token is valid: $_tokenIsValid'); + debugPrint('Created auth provider'); } + /// Initiates the OAuth sign-in process and updates fields accordingly upon successful login + @action Future login() async { + // Create the OAuth sign-in URI final loginUrl = Uri( scheme: 'https', host: 'id.twitch.tv', path: '/oauth2/authorize', queryParameters: { - 'client_id': _clientId, + 'client_id': clientId, 'redirect_uri': 'auth://', 'response_type': 'token', 'scope': 'chat:read chat:edit user:read:follows', @@ -116,30 +88,37 @@ abstract class AuthBase with Store { ); try { + // Retrieve the OAuth redirect URI final result = await FlutterWebAuth.authenticate(url: loginUrl.toString(), callbackUrlScheme: 'auth', preferEphemeral: true); + // Parse the user token from the redirect URI fragment final fragment = Uri.parse(result).fragment; - _token = fragment.substring(fragment.indexOf('=') + 1, fragment.indexOf('&')); - await _storage.write(key: 'USER_TOKEN', value: token); + // Store the user token + await _storage.write(key: 'USER_TOKEN', value: _token); + + // Retrieve the user info and set it + _user = await Twitch.getUserInfo(headers: headersTwitch); + + // Set the login status to logged in _isLoggedIn = true; - authHeaders = {'Authorization': 'Bearer $token', 'Client-Id': _clientId}; - - await _getUserInfo(); - - // notifyListeners(); } catch (error) { debugPrint('Login failed due to $error'); } } - void logout() async { - await _getDefaultToken(); - _isLoggedIn = false; + // Logs out the current user and updates fields accordingly + @action + Future logout() async { + // If the default token already exists, set it. Otherwise, get a new default token. + _token = await _storage.read(key: 'DEFAULT_TOKEN') ?? await Twitch.getDefaultToken(); - await _storage.delete(key: 'USER_TOKEN'); - debugPrint('Succesfully logged out'); - // notifyListeners(); + // Clear the user info + _user = null; + + // Set the login status to logged out + _isLoggedIn = false; + debugPrint('Successfully logged out'); } } diff --git a/lib/stores/auth_store.g.dart b/lib/stores/auth_store.g.dart index 652e77bc..7e4b53cd 100644 --- a/lib/stores/auth_store.g.dart +++ b/lib/stores/auth_store.g.dart @@ -8,8 +8,31 @@ part of 'auth_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 -mixin _$AuthStore on AuthBase, Store { - final _$_isLoggedInAtom = Atom(name: 'AuthBase._isLoggedIn'); +mixin _$AuthStore on _AuthBase, Store { + Computed>? _$headersTwitchComputed; + + @override + Map get headersTwitch => (_$headersTwitchComputed ??= + Computed>(() => super.headersTwitch, + name: '_AuthBase.headersTwitch')) + .value; + + final _$_tokenAtom = Atom(name: '_AuthBase._token'); + + @override + String? get _token { + _$_tokenAtom.reportRead(); + return super._token; + } + + @override + set _token(String? value) { + _$_tokenAtom.reportWrite(value, super._token, () { + super._token = value; + }); + } + + final _$_isLoggedInAtom = Atom(name: '_AuthBase._isLoggedIn'); @override bool get _isLoggedIn { @@ -24,10 +47,61 @@ mixin _$AuthStore on AuthBase, Store { }); } + final _$_tokenIsValidAtom = Atom(name: '_AuthBase._tokenIsValid'); + + @override + bool get _tokenIsValid { + _$_tokenIsValidAtom.reportRead(); + return super._tokenIsValid; + } + + @override + set _tokenIsValid(bool value) { + _$_tokenIsValidAtom.reportWrite(value, super._tokenIsValid, () { + super._tokenIsValid = value; + }); + } + + final _$_userAtom = Atom(name: '_AuthBase._user'); + + @override + UserTwitch? get _user { + _$_userAtom.reportRead(); + return super._user; + } + + @override + set _user(UserTwitch? value) { + _$_userAtom.reportWrite(value, super._user, () { + super._user = value; + }); + } + + final _$initAsyncAction = AsyncAction('_AuthBase.init'); + + @override + Future init() { + return _$initAsyncAction.run(() => super.init()); + } + + final _$loginAsyncAction = AsyncAction('_AuthBase.login'); + + @override + Future login() { + return _$loginAsyncAction.run(() => super.login()); + } + + final _$logoutAsyncAction = AsyncAction('_AuthBase.logout'); + + @override + Future logout() { + return _$logoutAsyncAction.run(() => super.logout()); + } + @override String toString() { return ''' - +headersTwitch: ${headersTwitch} '''; } } From ca482ceb0529053dadeccab7061309c20fe7db61 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 22:14:02 -0400 Subject: [PATCH 12/15] Refactor channel list store --- lib/stores/channel_list_store.dart | 156 ++++++++------------------- lib/stores/channel_list_store.g.dart | 86 ++------------- 2 files changed, 55 insertions(+), 187 deletions(-) diff --git a/lib/stores/channel_list_store.dart b/lib/stores/channel_list_store.dart index fc8f251c..6b105563 100644 --- a/lib/stores/channel_list_store.dart +++ b/lib/stores/channel_list_store.dart @@ -1,9 +1,6 @@ +import 'package:frosty/api/twitch_api.dart'; import 'package:mobx/mobx.dart'; -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:frosty/constants.dart'; import 'package:frosty/stores/auth_store.dart'; -import 'package:http/http.dart' as http; import 'package:frosty/models/channel.dart'; part 'channel_list_store.g.dart'; @@ -17,140 +14,81 @@ abstract class _ChannelListBase with Store { @observable ObservableList followedChannels = ObservableList(); - @observable bool isLoading = false; - @observable String? topChannelsCurrentCursor; - @observable String? followedChannelsCurrentCursor; - final token = AuthBase.token; - final String? id; - - _ChannelListBase({this.id}) { - if (token != null) { - updateTopChannels(); - } - if (id != null) { - updateFollowedChannels(); - } - } - - String? currentCursor({required Category category}) { + ObservableList channels({required ChannelCategory category}) { switch (category) { - case Category.top: - return topChannelsCurrentCursor; - case Category.followed: - return followedChannelsCurrentCursor; - } - } - - ObservableList channels({required Category category}) { - switch (category) { - case Category.top: + case ChannelCategory.top: return topChannels; - case Category.followed: + case ChannelCategory.followed: return followedChannels; } } - @action - Future update({required Category category}) async { + final AuthStore auth; + + _ChannelListBase({required this.auth}) { + getChannels(category: ChannelCategory.top); + + if (auth.user?.id != null) { + getChannels(category: ChannelCategory.followed); + } + } + + bool hasMore({required ChannelCategory category}) { switch (category) { - case Category.top: - await updateTopChannels(); - break; - case Category.followed: - await updateFollowedChannels(); - break; + case ChannelCategory.top: + return isLoading == false && topChannelsCurrentCursor != null; + case ChannelCategory.followed: + return isLoading == false && followedChannelsCurrentCursor != null; } } - /// Returns the top 10 streamers and a cursor for further requests. @action - Future updateTopChannels() async { - final url = Uri.parse('https://api.twitch.tv/helix/streams?first=10'); - final headers = {'Authorization': 'Bearer $token', 'Client-Id': clientId}; - - final response = await http.get(url, headers: headers); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body); - final data = decoded['data'] as List; - - final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; - - topChannels = ObservableList.of(result['channels']); - topChannelsCurrentCursor = result['cursor']; - } else { - debugPrint('Failed to update top channels'); + Future refresh({required ChannelCategory category}) async { + switch (category) { + case ChannelCategory.top: + topChannelsCurrentCursor = null; + topChannels.clear(); + break; + case ChannelCategory.followed: + followedChannelsCurrentCursor = null; + followedChannels.clear(); + break; } - // notifyListeners(); + + await getChannels(category: category); } @action - Future getMoreChannels({required Category category}) async { + Future getChannels({required ChannelCategory category}) async { isLoading = true; - Uri url; switch (category) { - case Category.top: - url = Uri.parse('https://api.twitch.tv/helix/streams?first=10&after=$topChannelsCurrentCursor'); + case ChannelCategory.top: + final newTopChannels = await Twitch.getTopChannels(headers: auth.headersTwitch, cursor: topChannelsCurrentCursor); + + if (newTopChannels != null) { + topChannels.addAll(newTopChannels['channels']); + topChannelsCurrentCursor = newTopChannels['cursor']; + } break; - case Category.followed: - url = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id&first=10&after=$followedChannelsCurrentCursor'); + case ChannelCategory.followed: + final newFollowedChannels = await Twitch.getFollowedChannels(id: auth.user!.id, headers: auth.headersTwitch, cursor: followedChannelsCurrentCursor); + + if (newFollowedChannels != null) { + followedChannels.addAll(newFollowedChannels['channels']); + followedChannelsCurrentCursor = newFollowedChannels['cursor']; + } break; } - final headers = {'Authorization': 'Bearer $token', 'Client-Id': clientId}; - final response = await http.get(url, headers: headers); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body); - final data = decoded['data'] as List; - - final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; - - switch (category) { - case Category.top: - topChannels.addAll(result['channels']); - topChannelsCurrentCursor = result['cursor']; - break; - case Category.followed: - followedChannels.addAll(result['channels']); - followedChannelsCurrentCursor = result['cursor']; - debugPrint(followedChannelsCurrentCursor); - break; - } - } else { - debugPrint('Failed to get more channels'); - } isLoading = false; - // notifyListeners(); - } - - @action - Future updateFollowedChannels() async { - final url = Uri.parse('https://api.twitch.tv/helix/streams/followed?first=10&user_id=$id'); - final headers = {'Authorization': 'Bearer $token', 'Client-Id': clientId}; - - final response = await http.get(url, headers: headers); - - if (response.statusCode == 200) { - final decoded = jsonDecode(response.body); - final data = decoded['data'] as List; - - final result = {'channels': data.map((channel) => Channel.fromJson(channel)).toList(), 'cursor': decoded['pagination']['cursor']}; - - followedChannels = ObservableList.of(result['channels']); - followedChannelsCurrentCursor = result['cursor']; - } else { - debugPrint('Failed to update followed channels'); - } - // notifyListeners(); } } -enum Category { top, followed } +enum ChannelCategory { top, followed } diff --git a/lib/stores/channel_list_store.g.dart b/lib/stores/channel_list_store.g.dart index 4cf0274c..3a9d741a 100644 --- a/lib/stores/channel_list_store.g.dart +++ b/lib/stores/channel_list_store.g.dart @@ -40,96 +40,26 @@ mixin _$ChannelListStore on _ChannelListBase, Store { }); } - final _$isLoadingAtom = Atom(name: '_ChannelListBase.isLoading'); + final _$refreshAsyncAction = AsyncAction('_ChannelListBase.refresh'); @override - bool get isLoading { - _$isLoadingAtom.reportRead(); - return super.isLoading; + Future refresh({required ChannelCategory category}) { + return _$refreshAsyncAction.run(() => super.refresh(category: category)); } - @override - set isLoading(bool value) { - _$isLoadingAtom.reportWrite(value, super.isLoading, () { - super.isLoading = value; - }); - } - - final _$topChannelsCurrentCursorAtom = - Atom(name: '_ChannelListBase.topChannelsCurrentCursor'); + final _$getChannelsAsyncAction = AsyncAction('_ChannelListBase.getChannels'); @override - String? get topChannelsCurrentCursor { - _$topChannelsCurrentCursorAtom.reportRead(); - return super.topChannelsCurrentCursor; - } - - @override - set topChannelsCurrentCursor(String? value) { - _$topChannelsCurrentCursorAtom - .reportWrite(value, super.topChannelsCurrentCursor, () { - super.topChannelsCurrentCursor = value; - }); - } - - final _$followedChannelsCurrentCursorAtom = - Atom(name: '_ChannelListBase.followedChannelsCurrentCursor'); - - @override - String? get followedChannelsCurrentCursor { - _$followedChannelsCurrentCursorAtom.reportRead(); - return super.followedChannelsCurrentCursor; - } - - @override - set followedChannelsCurrentCursor(String? value) { - _$followedChannelsCurrentCursorAtom - .reportWrite(value, super.followedChannelsCurrentCursor, () { - super.followedChannelsCurrentCursor = value; - }); - } - - final _$updateAsyncAction = AsyncAction('_ChannelListBase.update'); - - @override - Future update({required Category category}) { - return _$updateAsyncAction.run(() => super.update(category: category)); - } - - final _$updateTopChannelsAsyncAction = - AsyncAction('_ChannelListBase.updateTopChannels'); - - @override - Future updateTopChannels() { - return _$updateTopChannelsAsyncAction.run(() => super.updateTopChannels()); - } - - final _$getMoreChannelsAsyncAction = - AsyncAction('_ChannelListBase.getMoreChannels'); - - @override - Future getMoreChannels({required Category category}) { - return _$getMoreChannelsAsyncAction - .run(() => super.getMoreChannels(category: category)); - } - - final _$updateFollowedChannelsAsyncAction = - AsyncAction('_ChannelListBase.updateFollowedChannels'); - - @override - Future updateFollowedChannels() { - return _$updateFollowedChannelsAsyncAction - .run(() => super.updateFollowedChannels()); + Future getChannels({required ChannelCategory category}) { + return _$getChannelsAsyncAction + .run(() => super.getChannels(category: category)); } @override String toString() { return ''' topChannels: ${topChannels}, -followedChannels: ${followedChannels}, -isLoading: ${isLoading}, -topChannelsCurrentCursor: ${topChannelsCurrentCursor}, -followedChannelsCurrentCursor: ${followedChannelsCurrentCursor} +followedChannels: ${followedChannels} '''; } } From 9bcefba327281a45edb8542114d08ba6368c7def Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 22:14:55 -0400 Subject: [PATCH 13/15] Update chat store compatibility --- lib/stores/chat_store.dart | 33 +++++++++++++++++++-------------- lib/stores/chat_store.g.dart | 6 +++--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/stores/chat_store.dart b/lib/stores/chat_store.dart index dfea7d11..43006080 100644 --- a/lib/stores/chat_store.dart +++ b/lib/stores/chat_store.dart @@ -1,9 +1,12 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; +import 'package:frosty/api/bttv_api.dart'; +import 'package:frosty/api/ffz_api.dart'; +import 'package:frosty/api/seventv_api.dart'; +import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/models/channel.dart'; import 'package:frosty/stores/auth_store.dart'; -import 'package:frosty/utility/request.dart'; import 'package:frosty/widgets/chat_message.dart'; import 'package:mobx/mobx.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; @@ -26,9 +29,11 @@ abstract class _ChatStoreBase with Store { final scrollController = ScrollController(); - _ChatStoreBase({required this.channelInfo}) { + final AuthStore auth; + + _ChatStoreBase({required this.auth, required this.channelInfo}) { final commands = [ - 'PASS oauth:${AuthBase.token}', + 'PASS oauth:${auth.token}', 'NICK justinfan888', 'CAP REQ :twitch.tv/tags', 'CAP REQ :twitch.tv/commands', @@ -51,18 +56,18 @@ abstract class _ChatStoreBase with Store { } @action - Future getEmotes() async { + Future getAssets() async { final assets = [ - await Request.getEmotesBTTVGlobal(), - await Request.getEmotesBTTVChannel(id: channelInfo.userId), - await Request.getEmotesFFZGlobal(), - await Request.getEmotesFFZChannel(id: channelInfo.userId), - await Request.getEmotesTwitchGlobal(), - await Request.getEmotesTwitchChannel(id: channelInfo.userId), - await Request.getBadgesTwitchGlobal(), - await Request.getBadgesTwitchChannel(id: channelInfo.userId), - await Request.getEmotes7TVGlobal(), - await Request.getEmotes7TVChannel(user: channelInfo.userLogin) + await FFZ.getEmotesGlobal(), + await FFZ.getEmotesChannel(id: channelInfo.userId), + await BTTV.getEmotesGlobal(), + await BTTV.getEmotesChannel(id: channelInfo.userId), + await Twitch.getEmotesGlobal(headers: auth.headersTwitch), + await Twitch.getEmotesChannel(id: channelInfo.userId, headers: auth.headersTwitch), + await Twitch.getBadgesGlobal(headers: auth.headersTwitch), + await Twitch.getBadgesChannel(id: channelInfo.userId, headers: auth.headersTwitch), + await SevenTV.getEmotesGlobal(), + await SevenTV.getEmotesChannel(user: channelInfo.userLogin) ]; for (final map in assets) { diff --git a/lib/stores/chat_store.g.dart b/lib/stores/chat_store.g.dart index 1ceb996f..b609919b 100644 --- a/lib/stores/chat_store.g.dart +++ b/lib/stores/chat_store.g.dart @@ -24,11 +24,11 @@ mixin _$ChatStore on _ChatStoreBase, Store { }); } - final _$getEmotesAsyncAction = AsyncAction('_ChatStoreBase.getEmotes'); + final _$getAssetsAsyncAction = AsyncAction('_ChatStoreBase.getAssets'); @override - Future getEmotes() { - return _$getEmotesAsyncAction.run(() => super.getEmotes()); + Future getAssets() { + return _$getAssetsAsyncAction.run(() => super.getAssets()); } final _$_ChatStoreBaseActionController = From 605ed5a40cc38ba5c4dfc0533928db74fdd1c341 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 22:15:58 -0400 Subject: [PATCH 14/15] Move scaffold to video/chat screen --- lib/screens/video_chat.dart | 25 ++++++++++++++----------- lib/widgets/channel_card.dart | 4 +--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/screens/video_chat.dart b/lib/screens/video_chat.dart index 3022cc07..6fb028bf 100644 --- a/lib/screens/video_chat.dart +++ b/lib/screens/video_chat.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:frosty/models/channel.dart'; import 'package:frosty/widgets/chat.dart'; @@ -10,17 +11,19 @@ class VideoChat extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - children: [ - AspectRatio( - aspectRatio: 16 / 9, - child: Video(channelName: channelInfo.userLogin), - ), - Expanded( - child: Chat(channelInfo: channelInfo), - ) - ], + return Scaffold( + body: SafeArea( + child: Column( + children: [ + AspectRatio( + aspectRatio: 16 / 9, + child: Video(channelName: channelInfo.userLogin), + ), + Expanded( + child: Chat(channelInfo: channelInfo), + ) + ], + ), ), ); } diff --git a/lib/widgets/channel_card.dart b/lib/widgets/channel_card.dart index 7714bd3d..025eed29 100644 --- a/lib/widgets/channel_card.dart +++ b/lib/widgets/channel_card.dart @@ -20,9 +20,7 @@ class ChannelCard extends StatelessWidget { context, CupertinoPageRoute( builder: (context) { - return Scaffold( - body: VideoChat(channelInfo: channelInfo), - ); + return VideoChat(channelInfo: channelInfo); }, ), ); From ae47476b0dd9d5459c541dc71761f8e316ee4834 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 3 Oct 2021 23:54:27 -0400 Subject: [PATCH 15/15] Fix flash when refreshing --- lib/screens/channel_list.dart | 18 +++++++++--------- lib/stores/channel_list_store.dart | 14 ++++++++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/screens/channel_list.dart b/lib/screens/channel_list.dart index 97aa2c5a..9fc41cce 100644 --- a/lib/screens/channel_list.dart +++ b/lib/screens/channel_list.dart @@ -19,11 +19,11 @@ class _ChannelListState extends State with AutomaticKeepAliveClient debugPrint('build channel list'); super.build(context); - return Observer( - builder: (_) { - final channels = widget.channelListStore.channels(category: widget.category); - return RefreshIndicator( - child: ListView.builder( + return RefreshIndicator( + child: Observer( + builder: (_) { + final channels = widget.channelListStore.channels(category: widget.category); + return ListView.builder( itemCount: channels.length, padding: const EdgeInsets.all(5.0), itemBuilder: (context, index) { @@ -32,10 +32,10 @@ class _ChannelListState extends State with AutomaticKeepAliveClient } return ChannelCard(channelInfo: channels.elementAt(index)); }, - ), - onRefresh: () => widget.channelListStore.refresh(category: widget.category), - ); - }, + ); + }, + ), + onRefresh: () => widget.channelListStore.refresh(category: widget.category), ); } diff --git a/lib/stores/channel_list_store.dart b/lib/stores/channel_list_store.dart index 6b105563..fe6ede77 100644 --- a/lib/stores/channel_list_store.dart +++ b/lib/stores/channel_list_store.dart @@ -53,11 +53,9 @@ abstract class _ChannelListBase with Store { switch (category) { case ChannelCategory.top: topChannelsCurrentCursor = null; - topChannels.clear(); break; case ChannelCategory.followed: followedChannelsCurrentCursor = null; - followedChannels.clear(); break; } @@ -73,7 +71,11 @@ abstract class _ChannelListBase with Store { final newTopChannels = await Twitch.getTopChannels(headers: auth.headersTwitch, cursor: topChannelsCurrentCursor); if (newTopChannels != null) { - topChannels.addAll(newTopChannels['channels']); + if (topChannelsCurrentCursor == null) { + topChannels = ObservableList.of(newTopChannels['channels']); + } else { + topChannels.addAll(newTopChannels['channels']); + } topChannelsCurrentCursor = newTopChannels['cursor']; } break; @@ -81,7 +83,11 @@ abstract class _ChannelListBase with Store { final newFollowedChannels = await Twitch.getFollowedChannels(id: auth.user!.id, headers: auth.headersTwitch, cursor: followedChannelsCurrentCursor); if (newFollowedChannels != null) { - followedChannels.addAll(newFollowedChannels['channels']); + if (followedChannelsCurrentCursor == null) { + followedChannels = ObservableList.of(newFollowedChannels['channels']); + } else { + followedChannels.addAll(newFollowedChannels['channels']); + } followedChannelsCurrentCursor = newFollowedChannels['cursor']; } break;