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} + '''; + } +}