diff --git a/lib/api/bttv_api.dart b/lib/api/bttv_api.dart index 9d54f5e2..86f93bd1 100644 --- a/lib/api/bttv_api.dart +++ b/lib/api/bttv_api.dart @@ -1,14 +1,19 @@ import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; -import 'package:http/http.dart' as http; +import 'package:http/http.dart'; + +class BTTVApi { + final Client _client; + + BTTVApi(this._client); -class BTTV { /// Returns a map of global BTTV emotes to their URL. - static Future> getEmotesGlobal() async { + Future> getEmotesGlobal() async { final url = Uri.parse('https://api.betterttv.net/3/cached/emotes/global'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; @@ -22,9 +27,9 @@ class BTTV { } /// Returns a map of a channel's BTTV emotes to their URL. - static Future> getEmotesChannel({required String id}) async { + 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); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -41,9 +46,9 @@ class BTTV { } } - static Future?> getBadges() async { + Future?> getBadges() async { final url = Uri.parse('https://api.betterttv.net/3/cached/badges'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final badges = jsonDecode(response.body) as List; diff --git a/lib/api/ffz_api.dart b/lib/api/ffz_api.dart index 783eac4e..5fa3b7b9 100644 --- a/lib/api/ffz_api.dart +++ b/lib/api/ffz_api.dart @@ -1,15 +1,20 @@ import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; -import 'package:http/http.dart' as http; +import 'package:http/http.dart'; import 'package:tuple/tuple.dart'; -class FFZ { +class FFZApi { + final Client _client; + + FFZApi(this._client); + /// Returns a map of global FFZ emotes to their URL. - static Future> getEmotesGlobal() async { + Future> getEmotesGlobal() async { final url = Uri.parse('https://api.frankerfacez.com/v1/set/global'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -29,9 +34,9 @@ class FFZ { } /// Returns a channel's FFZ room info including custom badges and emote used. - static Future>?> getRoomInfo({required String name}) async { + Future>?> getRoomInfo({required String name}) async { final url = Uri.parse('https://api.frankerfacez.com/v1/room/$name'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -46,9 +51,9 @@ class FFZ { } } - static Future>?> getBadges() async { + Future>?> getBadges() async { final url = Uri.parse('https://api.frankerfacez.com/v1/badges/ids'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); diff --git a/lib/api/seventv_api.dart b/lib/api/seventv_api.dart index 730be9db..5a8b6d14 100644 --- a/lib/api/seventv_api.dart +++ b/lib/api/seventv_api.dart @@ -1,15 +1,20 @@ import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:frosty/models/badges.dart'; import 'package:frosty/models/emotes.dart'; -import 'package:http/http.dart' as http; +import 'package:http/http.dart'; // A class for general requests. -class SevenTV { +class SevenTVAPI { + final Client _client; + + SevenTVAPI(this._client); + /// Returns a map of global 7TV emotes to their URL. - static Future> getEmotesGlobal() async { + Future> getEmotesGlobal() async { final url = Uri.parse('https://api.7tv.app/v2/emotes/global'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; @@ -23,9 +28,9 @@ class SevenTV { } /// Returns a map of a channel's 7TV emotes to their URL. - static Future> getEmotesChannel({required String user}) async { + Future> getEmotesChannel({required String user}) async { final url = Uri.parse('https://api.7tv.app/v2/users/$user/emotes'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body) as List; @@ -38,9 +43,9 @@ class SevenTV { } } - static Future>?> getBadges() async { + Future>?> getBadges() async { final url = Uri.parse('https://api.7tv.app/v2/badges?user_identifier=twitch_id'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['badges'] as List; diff --git a/lib/api/twitch_api.dart b/lib/api/twitch_api.dart index 5910855e..a020a05c 100644 --- a/lib/api/twitch_api.dart +++ b/lib/api/twitch_api.dart @@ -9,12 +9,16 @@ import 'package:frosty/models/chatters.dart'; import 'package:frosty/models/emotes.dart'; import 'package:frosty/models/stream.dart'; import 'package:frosty/models/user.dart'; -import 'package:http/http.dart' as http; +import 'package:http/http.dart'; -class Twitch { - static Future> getEmotesGlobal({required Map? headers}) async { +class TwitchApi { + final Client _client; + + TwitchApi(this._client); + + 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); + final response = await _client.get(url, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; @@ -28,9 +32,9 @@ class Twitch { } /// Returns a map of a channel's Twitch emotes to their URL. - static Future> getEmotesChannel({required String id, required Map? headers}) async { + 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); + final response = await _client.get(url, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; @@ -44,9 +48,9 @@ class Twitch { } /// Returns a map of a channel's Twitch emotes to their URL. - static Future> getEmotesSets({required String setId, required Map? headers, sub = false}) async { + Future> getEmotesSets({required String setId, required Map? headers, sub = false}) async { final url = Uri.parse('https://api.twitch.tv/helix/chat/emotes/set?emote_set_id=$setId'); - final response = await http.get(url, headers: headers); + final response = await _client.get(url, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body)['data'] as List; @@ -69,9 +73,9 @@ class Twitch { } /// Returns a map of global Twitch badges to their URL. - static Future> getBadgesGlobal() async { + Future> getBadgesGlobal() async { final url = Uri.parse('https://badges.twitch.tv/v1/badges/global/display'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final result = {}; @@ -89,9 +93,9 @@ class Twitch { } /// Returns a map of a channel's Twitch badges to their URL. - static Future> getBadgesChannel({required String id}) async { + Future> getBadgesChannel({required String id}) async { final url = Uri.parse('https://badges.twitch.tv/v1/badges/channels/$id/display'); - final response = await http.get(url); + final response = await _client.get(url); if (response.statusCode == 200) { final result = {}; @@ -109,8 +113,8 @@ class Twitch { } /// 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); + Future getUserInfo({required Map? headers}) async { + final response = await _client.get(Uri.parse('https://api.twitch.tv/helix/users'), headers: headers); if (response.statusCode == 200) { final userData = jsonDecode(response.body)['data'] as List; @@ -122,7 +126,7 @@ class Twitch { } /// Returns a token for an anonymous user. - static Future getDefaultToken() async { + Future getDefaultToken() async { debugPrint('Getting default token...'); final url = Uri( @@ -136,7 +140,7 @@ class Twitch { }, ); - final response = await http.post(url); + final response = await _client.post(url); if (response.statusCode == 200) { return jsonDecode(response.body)['access_token']; @@ -146,9 +150,9 @@ class Twitch { } /// Returns the validity of the given token - static Future validateToken({required String token}) async { + Future validateToken({required String token}) async { debugPrint('Validating token...'); - final response = await http.get( + final response = await _client.get( Uri.parse('https://id.twitch.tv/oauth2/validate'), headers: {'Authorization': 'Bearer $token'}, ); @@ -163,7 +167,7 @@ class Twitch { } /// Returns a map containing top 20 streams and a cursor for further requests. - static Future getTopStreams({required Map? headers, String? cursor}) async { + Future getTopStreams({required Map? headers, String? cursor}) async { final Uri uri; if (cursor == null) { @@ -172,7 +176,7 @@ class Twitch { uri = Uri.parse('https://api.twitch.tv/helix/streams?after=$cursor'); } - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -184,7 +188,7 @@ class Twitch { } /// Returns a map with the given user's top 20 followed streams and a cursor for further requests. - static Future getFollowedStreams({ + Future getFollowedStreams({ required String id, required Map? headers, String? cursor, @@ -197,7 +201,7 @@ class Twitch { uri = Uri.parse('https://api.twitch.tv/helix/streams/followed?user_id=$id&after=$cursor'); } - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -209,7 +213,7 @@ class Twitch { } /// Returns the list of streams under the given game/category ID. - static Future getStreamsUnderGame({ + Future getStreamsUnderGame({ required String gameId, required Map? headers, String? cursor, @@ -218,7 +222,7 @@ class Twitch { ? Uri.parse('https://api.twitch.tv/helix/streams?game_id=$gameId') : Uri.parse('https://api.twitch.tv/helix/streams?game_id=$gameId&after=$cursor'); - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -230,10 +234,10 @@ class Twitch { } /// Returns the stream info given the user login. - static Future getStream({required String userLogin, required Map? headers}) async { + Future getStream({required String userLogin, required Map? headers}) async { final uri = Uri.parse('https://api.twitch.tv/helix/streams?user_login=$userLogin'); - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -248,10 +252,10 @@ class Twitch { } /// Returns a user's info given their login name. - static Future getUser({String? userLogin, String? id, required Map? headers}) async { + Future getUser({String? userLogin, String? id, required Map? headers}) async { final uri = id != null ? Uri.parse('https://api.twitch.tv/helix/users?id=$id') : Uri.parse('https://api.twitch.tv/helix/users?login=$userLogin'); - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final userData = jsonDecode(response.body)['data'] as List; @@ -266,8 +270,8 @@ class Twitch { } /// Returns a channels's info associated with the given ID. - static Future getChannel({required String userId, required Map? headers}) async { - final response = await http.get(Uri.parse('https://api.twitch.tv/helix/channels?broadcaster_id=$userId'), headers: headers); + Future getChannel({required String userId, required Map? headers}) async { + final response = await _client.get(Uri.parse('https://api.twitch.tv/helix/channels?broadcaster_id=$userId'), headers: headers); if (response.statusCode == 200) { final channelData = jsonDecode(response.body)['data'] as List; @@ -282,8 +286,8 @@ class Twitch { } /// Returns a list of channel query objects closest matching the given query string. - static Future> searchChannels({required String query, required Map? headers}) async { - final response = await http.get(Uri.parse('https://api.twitch.tv/helix/search/channels?first=8&query=$query'), headers: headers); + Future> searchChannels({required String query, required Map? headers}) async { + final response = await _client.get(Uri.parse('https://api.twitch.tv/helix/search/channels?first=8&query=$query'), headers: headers); if (response.statusCode == 200) { final channelData = jsonDecode(response.body)['data'] as List; @@ -294,7 +298,7 @@ class Twitch { } /// Returns a map containing top 20 categories/games and a cursor for further requests. - static Future getTopGames({required Map? headers, String? cursor}) async { + Future getTopGames({required Map? headers, String? cursor}) async { final Uri uri; if (cursor == null) { @@ -303,7 +307,7 @@ class Twitch { uri = Uri.parse('https://api.twitch.tv/helix/games/top?after=$cursor'); } - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -315,12 +319,12 @@ class Twitch { } /// Returns a map containing top 20 categories/games and a cursor for further requests. - static Future searchCategories({required Map? headers, required String query, String? cursor}) async { + Future searchCategories({required Map? headers, required String query, String? cursor}) async { final uri = cursor == null ? Uri.parse('https://api.twitch.tv/helix/search/categories?first=8&query=$query') : Uri.parse('https://api.twitch.tv/helix/search/categories?first=8&query=$query&after=$cursor'); - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -332,10 +336,10 @@ class Twitch { } /// Returns the sub count for a user. - static Future getSubscriberCount({required String userId, required Map? headers}) async { + Future getSubscriberCount({required String userId, required Map? headers}) async { final uri = Uri.parse('https://api.twitch.tv/helix/subscriptions?broadcaster_id=$userId'); - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -346,10 +350,10 @@ class Twitch { } } - static Future getChatters({required String userLogin}) async { + Future getChatters({required String userLogin}) async { final uri = Uri.parse('https://tmi.twitch.tv/group/user/$userLogin/chatters'); - final response = await http.get(uri); + final response = await _client.get(uri); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -361,7 +365,7 @@ class Twitch { } /// Returns a user's list of blocked users given their id. - static Future> getUserBlockedList({ + Future> getUserBlockedList({ required String id, required Map? headers, String? cursor, @@ -370,7 +374,7 @@ class Twitch { ? Uri.parse('https://api.twitch.tv/helix/users/blocks?first=100&broadcaster_id=$id') : Uri.parse('https://api.twitch.tv/helix/users/blocks?first=100&broadcaster_id=$id&after=$cursor'); - final response = await http.get(uri, headers: headers); + final response = await _client.get(uri, headers: headers); if (response.statusCode == 200) { final decoded = jsonDecode(response.body); @@ -396,10 +400,10 @@ class Twitch { } // Blocks the user with the given ID and returns true on success or false on failure. - static Future blockUser({required String userId, required Map headers}) async { + Future blockUser({required String userId, required Map headers}) async { final uri = Uri.parse('https://api.twitch.tv/helix/users/blocks?target_user_id=$userId'); - final response = await http.put(uri, headers: headers); + final response = await _client.put(uri, headers: headers); if (response.statusCode == 204) { return true; } else { @@ -408,10 +412,10 @@ class Twitch { } // Unblocks the user with the given ID and returns true on success or false on failure. - static Future unblockUser({required String userId, required Map headers}) async { + Future unblockUser({required String userId, required Map headers}) async { final uri = Uri.parse('https://api.twitch.tv/helix/users/blocks?target_user_id=$userId'); - final response = await http.delete(uri, headers: headers); + final response = await _client.delete(uri, headers: headers); if (response.statusCode == 204) { return true; } else { diff --git a/lib/core/auth/auth_store.dart b/lib/core/auth/auth_store.dart index 584179aa..f72f059c 100644 --- a/lib/core/auth/auth_store.dart +++ b/lib/core/auth/auth_store.dart @@ -14,11 +14,13 @@ abstract class _AuthBase with Store { /// Secure storage to store tokens. final _storage = const FlutterSecureStorage(); + final TwitchApi twitchApi; + /// Whether the token is valid or not. var _tokenIsValid = false; /// The store containing information relevant to the current user. - final user = UserStore(); + final UserStore user; /// The current token. @readonly @@ -32,6 +34,8 @@ abstract class _AuthBase with Store { @computed Map get headersTwitch => {'Authorization': 'Bearer $_token', 'Client-Id': clientId}; + _AuthBase({required this.twitchApi}) : user = UserStore(twitchApi: twitchApi); + /// Initialize by retrieving a token if it does not already exist. @action Future init() async { @@ -45,7 +49,7 @@ abstract class _AuthBase with Store { _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(); + _token = await twitchApi.getDefaultToken(); await _storage.write(key: 'DEFAULT_TOKEN', value: _token); } } else { @@ -56,7 +60,7 @@ abstract class _AuthBase with Store { } // Validate the token. - _tokenIsValid = await Twitch.validateToken(token: _token!); + _tokenIsValid = await twitchApi.validateToken(token: _token!); // If the token is invalid, logout. if (!_tokenIsValid) { @@ -98,7 +102,7 @@ abstract class _AuthBase with Store { _token = customToken; } - _tokenIsValid = await Twitch.validateToken(token: _token!); + _tokenIsValid = await twitchApi.validateToken(token: _token!); if (!_tokenIsValid) return; // Store the user token. @@ -133,18 +137,18 @@ abstract class _AuthBase with Store { // If the default token does not already exist, get the new default token and store it. // Else, validate the existing token. if (_token == null) { - _token = await Twitch.getDefaultToken(); + _token = await twitchApi.getDefaultToken(); await _storage.write(key: 'DEFAULT_TOKEN', value: _token); - _tokenIsValid = await Twitch.validateToken(token: _token!); + _tokenIsValid = await twitchApi.validateToken(token: _token!); } else { // Validate the stored token. - _tokenIsValid = await Twitch.validateToken(token: _token!); + _tokenIsValid = await twitchApi.validateToken(token: _token!); // If the stored token is invalid, get a new default token and store it. if (!_tokenIsValid) { - _token = await Twitch.getDefaultToken(); + _token = await twitchApi.getDefaultToken(); await _storage.write(key: 'DEFAULT_TOKEN', value: _token); - _tokenIsValid = await Twitch.validateToken(token: _token!); + _tokenIsValid = await twitchApi.validateToken(token: _token!); } } diff --git a/lib/core/user/user_store.dart b/lib/core/user/user_store.dart index a40dca24..17e4940c 100644 --- a/lib/core/user/user_store.dart +++ b/lib/core/user/user_store.dart @@ -7,6 +7,8 @@ part 'user_store.g.dart'; class UserStore = _UserStoreBase with _$UserStore; abstract class _UserStoreBase with Store { + final TwitchApi twitchApi; + /// The current user's info. @readonly UserTwitch? _details; @@ -17,20 +19,22 @@ abstract class _UserStoreBase with Store { ReactionDisposer? disposeReaction; + _UserStoreBase({required this.twitchApi}); + @action Future init({required Map headers}) async { // Get and update the current user's info. - _details = await Twitch.getUserInfo(headers: headers); + _details = await twitchApi.getUserInfo(headers: headers); // Get and update the current user's list of blocked users. - if (_details?.id != null) _blockedUsers = (await Twitch.getUserBlockedList(id: _details!.id, headers: headers)).asObservable(); + if (_details?.id != null) _blockedUsers = (await twitchApi.getUserBlockedList(id: _details!.id, headers: headers)).asObservable(); disposeReaction = autorun((_) => _blockedUsers.sort((a, b) => a.userLogin.compareTo(b.userLogin))); } @action Future block({required String targetId, required Map headers}) async { - final success = await Twitch.blockUser(userId: targetId, headers: headers); + final success = await twitchApi.blockUser(userId: targetId, headers: headers); // Add a slight delay between requests, otherwise the blocked list won't properly update. // Weird behavior, might be something to do with PUT request and time to create and update resource? await Future.delayed(const Duration(milliseconds: 200)); @@ -39,13 +43,13 @@ abstract class _UserStoreBase with Store { @action Future unblock({required String targetId, required Map headers}) async { - final success = await Twitch.unblockUser(userId: targetId, headers: headers); + final success = await twitchApi.unblockUser(userId: targetId, headers: headers); if (success) await refreshBlockedUsers(headers: headers); } @action Future refreshBlockedUsers({required Map headers}) async => - _blockedUsers = (await Twitch.getUserBlockedList(id: _details!.id, headers: headers)).asObservable(); + _blockedUsers = (await twitchApi.getUserBlockedList(id: _details!.id, headers: headers)).asObservable(); @action void dispose() { diff --git a/lib/main.dart b/lib/main.dart index ea302437..dc28edd1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,14 +3,20 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.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/constants/constants.dart'; import 'package:frosty/core/auth/auth_store.dart'; +import 'package:frosty/core/user/user_store.dart'; import 'package:frosty/screens/home/home.dart'; import 'package:frosty/screens/home/stores/categories_store.dart'; import 'package:frosty/screens/home/stores/home_store.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; import 'package:frosty/screens/home/stores/search_store.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; +import 'package:http/http.dart'; import 'package:mobx/mobx.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -19,10 +25,6 @@ import 'package:shared_preferences/shared_preferences.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - // Create and initialize the authentication store - final authStore = AuthStore(); - await authStore.init(); - // Get the shared preferences instance and obtain the existing user settings if it exists. // If default settings don't exist, use an empty JSON string to use the default values. final preferences = await SharedPreferences.getInstance(); @@ -38,11 +40,25 @@ Future main() async { await SentryFlutter.init((options) => options.tracesSampleRate = sampleRate); } + final client = Client(); + final twitchApiService = TwitchApi(client); + final bttvApiService = BTTVApi(client); + final ffzApiService = FFZApi(client); + final sevenTvApiService = SevenTVAPI(client); + + // Create and initialize the authentication store + final authStore = AuthStore(twitchApi: twitchApiService); + await authStore.init(); + runApp( MultiProvider( providers: [ Provider(create: (_) => authStore), Provider(create: (_) => settingsStore), + Provider(create: (_) => twitchApiService), + Provider(create: (_) => bttvApiService), + Provider(create: (_) => ffzApiService), + Provider(create: (_) => sevenTvApiService), ], child: const MyApp(), ), @@ -54,9 +70,6 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - final authStore = context.read(); - final settingsStore = context.read(); - final lightTheme = ThemeData( scaffoldBackgroundColor: Colors.white, splashFactory: Platform.isIOS ? NoSplash.splashFactory : null, @@ -130,6 +143,10 @@ class MyApp extends StatelessWidget { return Observer( builder: (context) { + final authStore = context.read(); + final settingsStore = context.read(); + final twitchApi = context.read(); + return MaterialApp( title: 'Frosty', theme: lightTheme, @@ -143,16 +160,24 @@ class MyApp extends StatelessWidget { homeStore: HomeStore(), topSectionStore: ListStore( authStore: authStore, + twitchApi: twitchApi, listType: ListType.top, ), - categoriesSectionStore: CategoriesStore(authStore: authStore), + categoriesSectionStore: CategoriesStore( + authStore: authStore, + twitchApi: twitchApi, + ), followedStreamsStore: authStore.isLoggedIn ? ListStore( authStore: authStore, + twitchApi: twitchApi, listType: ListType.followed, ) : null, - searchStore: SearchStore(authStore: authStore), + searchStore: SearchStore( + authStore: authStore, + twitchApi: twitchApi, + ), ), ); }, diff --git a/lib/screens/channel/stores/chat_assets_store.dart b/lib/screens/channel/stores/chat_assets_store.dart index c8ea6d4c..e7a0be16 100644 --- a/lib/screens/channel/stores/chat_assets_store.dart +++ b/lib/screens/channel/stores/chat_assets_store.dart @@ -12,6 +12,11 @@ part 'chat_assets_store.g.dart'; class ChatAssetsStore = _ChatAssetsStoreBase with _$ChatAssetsStore; abstract class _ChatAssetsStoreBase with Store { + final TwitchApi twitchApi; + final BTTVApi bttvApi; + final FFZApi ffzApi; + final SevenTVAPI sevenTvApi; + /// Contains any custom FFZ mod and vip badges for the channel. RoomFFZ? ffzRoomInfo; @@ -67,11 +72,18 @@ abstract class _ChatAssetsStoreBase with Store { return emote.type == EmoteType.sevenTvChannel || emote.type == EmoteType.sevenTvGlobal; } + _ChatAssetsStoreBase({ + required this.twitchApi, + required this.bttvApi, + required this.ffzApi, + required this.sevenTvApi, + }); + /// Fetches global and channel assets (badges and emotes) and stores them in [_emoteToUrl] @action Future getAssets({required String channelName, required Map headers}) async { // Fetch the desired channel/user's information. - final channelInfo = await Twitch.getUser(userLogin: channelName, headers: headers); + final channelInfo = await twitchApi.getUser(userLogin: channelName, headers: headers); if (channelInfo != null) { // Fetch the global and channel's assets (emotes & badges). @@ -87,14 +99,14 @@ abstract class _ChatAssetsStoreBase with Store { @action Future getEmotes({required UserTwitch channelInfo, required Map headers}) async { final assets = await Future.wait([ - FFZ.getEmotesGlobal(), - BTTV.getEmotesGlobal(), - BTTV.getEmotesChannel(id: channelInfo.id), - Twitch.getEmotesGlobal(headers: headers), - Twitch.getEmotesChannel(id: channelInfo.id, headers: headers), - SevenTV.getEmotesGlobal(), - SevenTV.getEmotesChannel(user: channelInfo.login), - FFZ.getRoomInfo(name: channelInfo.login).then((ffzRoom) { + ffzApi.getEmotesGlobal(), + bttvApi.getEmotesGlobal(), + bttvApi.getEmotesChannel(id: channelInfo.id), + twitchApi.getEmotesGlobal(headers: headers), + twitchApi.getEmotesChannel(id: channelInfo.id, headers: headers), + sevenTvApi.getEmotesGlobal(), + sevenTvApi.getEmotesChannel(user: channelInfo.login), + ffzApi.getRoomInfo(name: channelInfo.login).then((ffzRoom) { ffzRoomInfo = ffzRoom?.item1; return ffzRoom?.item2 ?? []; }), @@ -107,17 +119,18 @@ abstract class _ChatAssetsStoreBase with Store { @action Future getBadges({required UserTwitch channelInfo, required Map headers}) async => await Future.wait([ - Twitch.getBadgesGlobal() + twitchApi + .getBadgesGlobal() .then((badges) => twitchBadgesToObject.addAll(badges)) - .then((_) => Twitch.getBadgesChannel(id: channelInfo.id).then((badges) => twitchBadgesToObject.addAll(badges))), - FFZ.getBadges().then((badges) => _userToFFZBadges = badges ?? {}), - SevenTV.getBadges().then((badges) => _userTo7TVBadges = badges ?? {}), - BTTV.getBadges().then((badges) => _userToBTTVBadges = badges ?? {}), + .then((_) => twitchApi.getBadgesChannel(id: channelInfo.id).then((badges) => twitchBadgesToObject.addAll(badges))), + ffzApi.getBadges().then((badges) => _userToFFZBadges = badges ?? {}), + sevenTvApi.getBadges().then((badges) => _userTo7TVBadges = badges ?? {}), + bttvApi.getBadges().then((badges) => _userToBTTVBadges = badges ?? {}), ]); @action Future getUserEmotes({required List emoteSets, required Map headers}) async => - await Future.wait(emoteSets.map((setId) => Twitch.getEmotesSets(setId: setId, headers: headers))) + await Future.wait(emoteSets.map((setId) => twitchApi.getEmotesSets(setId: setId, headers: headers))) .then((emotes) => emotes.expand((list) => list).toList()) .then((userEmotes) => _userEmoteToObject = {for (final emote in userEmotes) emote.name: emote}.asObservable()); } diff --git a/lib/screens/channel/stores/chat_details_store.dart b/lib/screens/channel/stores/chat_details_store.dart index e5334308..ff0a8806 100644 --- a/lib/screens/channel/stores/chat_details_store.dart +++ b/lib/screens/channel/stores/chat_details_store.dart @@ -8,6 +8,8 @@ part 'chat_details_store.g.dart'; class ChatDetailsStore = _ChatDetailsStoreBase with _$ChatDetailsStore; abstract class _ChatDetailsStoreBase with Store { + final TwitchApi twitchApi; + /// The rules and modes being used in the chat. @observable var roomState = const ROOMSTATE(); @@ -33,6 +35,8 @@ abstract class _ChatDetailsStoreBase with Store { _chatUsers!.chatters.viewers, ].map((e) => e.where((user) => user.contains(filterText)).toList()); + _ChatDetailsStoreBase({required this.twitchApi}); + @action - Future updateChatters(String userLogin) async => _chatUsers = await Twitch.getChatters(userLogin: userLogin); + Future updateChatters(String userLogin) async => _chatUsers = await twitchApi.getChatters(userLogin: userLogin); } diff --git a/lib/screens/channel/stores/chat_store.dart b/lib/screens/channel/stores/chat_store.dart index c552fa66..bd593526 100644 --- a/lib/screens/channel/stores/chat_store.dart +++ b/lib/screens/channel/stores/chat_store.dart @@ -46,10 +46,10 @@ abstract class _ChatStoreBase with Store { final textController = TextEditingController(); /// The chat details store responsible for the chat modes and users in chat. - final chatDetailsStore = ChatDetailsStore(); + final ChatDetailsStore chatDetailsStore; /// The assets store responsible for badges, emotes, and the emote menu. - final assetsStore = ChatAssetsStore(); + final ChatAssetsStore assetsStore; /// Requested message to be sent by the user. Will only be sent on receipt of a USERNOTICE command. IRCMessage? toSend; @@ -70,6 +70,8 @@ abstract class _ChatStoreBase with Store { _ChatStoreBase({ required this.auth, + required this.chatDetailsStore, + required this.assetsStore, required this.settings, required this.channelName, }) { diff --git a/lib/screens/channel/stores/video_store.dart b/lib/screens/channel/stores/video_store.dart index 08580221..3dfc7f16 100644 --- a/lib/screens/channel/stores/video_store.dart +++ b/lib/screens/channel/stores/video_store.dart @@ -13,6 +13,8 @@ part 'video_store.g.dart'; class VideoStore = _VideoStoreBase with _$VideoStore; abstract class _VideoStoreBase with Store { + final TwitchApi twitchApi; + WebViewController? controller; late Timer overlayTimer; @@ -32,6 +34,7 @@ abstract class _VideoStoreBase with Store { final SettingsStore settingsStore; _VideoStoreBase({ + required this.twitchApi, required this.userLogin, required this.authStore, required this.settingsStore, @@ -69,7 +72,7 @@ abstract class _VideoStoreBase with Store { @action Future updateStreamInfo() async { - final updatedStreamInfo = await Twitch.getStream(userLogin: userLogin, headers: authStore.headersTwitch); + final updatedStreamInfo = await twitchApi.getStream(userLogin: userLogin, headers: authStore.headersTwitch); if (updatedStreamInfo != null) { _streamInfo = updatedStreamInfo; } diff --git a/lib/screens/channel/video_chat.dart b/lib/screens/channel/video_chat.dart index 01555186..fceb2151 100644 --- a/lib/screens/channel/video_chat.dart +++ b/lib/screens/channel/video_chat.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/screens/channel/chat/chat.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; import 'package:frosty/screens/channel/stores/video_store.dart'; import 'package:frosty/screens/channel/video/video.dart'; import 'package:frosty/screens/settings/settings.dart'; +import 'package:provider/provider.dart'; class VideoChat extends StatefulWidget { final String displayName; @@ -31,6 +33,7 @@ class _VideoChatState extends State { key: GlobalKey(), userLogin: chatStore.channelName, videoStore: VideoStore( + twitchApi: context.read(), userLogin: chatStore.channelName, authStore: chatStore.auth, settingsStore: chatStore.settings, diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index d43f9c0a..e1d9938d 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -1,6 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.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/screens/channel/stores/chat_assets_store.dart'; +import 'package:frosty/screens/channel/stores/chat_details_store.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; import 'package:frosty/screens/channel/video_chat.dart'; import 'package:frosty/screens/home/stores/search_store.dart'; @@ -29,6 +35,15 @@ class SearchResultsChannels extends StatelessWidget { builder: (context) => VideoChat( displayName: channelInfo!.broadcasterName, chatStore: ChatStore( + chatDetailsStore: ChatDetailsStore( + twitchApi: context.read(), + ), + assetsStore: ChatAssetsStore( + twitchApi: context.read(), + ffzApi: context.read(), + bttvApi: context.read(), + sevenTvApi: context.read(), + ), channelName: channelInfo.broadcasterLogin, auth: searchStore.authStore, settings: context.read(), @@ -93,6 +108,15 @@ class SearchResultsChannels extends StatelessWidget { builder: (context) => VideoChat( displayName: channel.displayName, chatStore: ChatStore( + chatDetailsStore: ChatDetailsStore( + twitchApi: context.read(), + ), + assetsStore: ChatAssetsStore( + twitchApi: context.read(), + ffzApi: context.read(), + bttvApi: context.read(), + sevenTvApi: context.read(), + ), channelName: channel.broadcasterLogin, auth: searchStore.authStore, settings: context.read(), diff --git a/lib/screens/home/stores/categories_store.dart b/lib/screens/home/stores/categories_store.dart index 80d8e4dd..cd129f1a 100644 --- a/lib/screens/home/stores/categories_store.dart +++ b/lib/screens/home/stores/categories_store.dart @@ -29,7 +29,9 @@ abstract class _CategoriesStoreBase with Store { /// Returns whether or not there are more streams and loading status for pagination. bool get hasMore => _isLoading == false && _categoriesCursor != null; - _CategoriesStoreBase({required this.authStore}) { + final TwitchApi twitchApi; + + _CategoriesStoreBase({required this.authStore, required this.twitchApi}) { scrollController.addListener(() { if (scrollController.position.atEdge || scrollController.position.outOfRange) { showJumpButton = false; @@ -45,7 +47,7 @@ abstract class _CategoriesStoreBase with Store { Future getCategories() async { _isLoading = true; - final result = await Twitch.getTopGames(headers: authStore.headersTwitch, cursor: _categoriesCursor); + final result = await twitchApi.getTopGames(headers: authStore.headersTwitch, cursor: _categoriesCursor); if (result != null) { if (_categoriesCursor == null) { _categories = result.data.asObservable(); diff --git a/lib/screens/home/stores/list_store.dart b/lib/screens/home/stores/list_store.dart index b0db121a..40aef3eb 100644 --- a/lib/screens/home/stores/list_store.dart +++ b/lib/screens/home/stores/list_store.dart @@ -13,6 +13,8 @@ abstract class _ListStoreBase with Store { /// The authentication store. final AuthStore authStore; + final TwitchApi twitchApi; + final ListType listType; final CategoryTwitch? categoryInfo; @@ -38,6 +40,7 @@ abstract class _ListStoreBase with Store { _ListStoreBase({ required this.authStore, + required this.twitchApi, required this.listType, this.categoryInfo, }) { @@ -68,20 +71,20 @@ abstract class _ListStoreBase with Store { final StreamsTwitch? newStreams; switch (listType) { case ListType.followed: - newStreams = await Twitch.getFollowedStreams( + newStreams = await twitchApi.getFollowedStreams( id: authStore.user.details!.id, headers: authStore.headersTwitch, cursor: _streamsCursor, ); break; case ListType.top: - newStreams = await Twitch.getTopStreams( + newStreams = await twitchApi.getTopStreams( headers: authStore.headersTwitch, cursor: _streamsCursor, ); break; case ListType.category: - newStreams = await Twitch.getStreamsUnderGame( + newStreams = await twitchApi.getStreamsUnderGame( gameId: categoryInfo!.id, headers: authStore.headersTwitch, cursor: _streamsCursor, diff --git a/lib/screens/home/stores/search_store.dart b/lib/screens/home/stores/search_store.dart index 3c9be1ec..c5c52eaf 100644 --- a/lib/screens/home/stores/search_store.dart +++ b/lib/screens/home/stores/search_store.dart @@ -13,6 +13,8 @@ class SearchStore = _SearchStoreBase with _$SearchStore; abstract class _SearchStoreBase with Store { final AuthStore authStore; + final TwitchApi twitchApi; + @readonly var _searchHistory = ObservableList(); @@ -22,7 +24,7 @@ abstract class _SearchStoreBase with Store { @readonly ObservableFuture? _categoryFuture; - _SearchStoreBase({required this.authStore}) { + _SearchStoreBase({required this.authStore, required this.twitchApi}) { init(); } @@ -46,26 +48,30 @@ abstract class _SearchStoreBase with Store { _searchHistory.remove(query); _searchHistory.insert(0, query); - _channelFuture = Twitch.searchChannels( + _channelFuture = twitchApi + .searchChannels( query: query, headers: authStore.headersTwitch, - ).then( + ) + .then( (channels) { channels.sort((c1, c2) => c2.isLive ? 1 : -1); return channels; }, ).asObservable(); - _categoryFuture = Twitch.searchCategories( - query: query, - headers: authStore.headersTwitch, - ).asObservable(); + _categoryFuture = twitchApi + .searchCategories( + query: query, + headers: authStore.headersTwitch, + ) + .asObservable(); } Future searchChannel(String query) async { try { - final user = await Twitch.getUser(userLogin: query, headers: authStore.headersTwitch); - return await Twitch.getChannel(userId: user!.id, headers: authStore.headersTwitch); + final user = await twitchApi.getUser(userLogin: query, headers: authStore.headersTwitch); + return await twitchApi.getChannel(userId: user!.id, headers: authStore.headersTwitch); } catch (e) { debugPrint(e.toString()); rethrow; diff --git a/lib/screens/home/widgets/category_card.dart b/lib/screens/home/widgets/category_card.dart index 1dace66f..363d8095 100644 --- a/lib/screens/home/widgets/category_card.dart +++ b/lib/screens/home/widgets/category_card.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:frosty/api/twitch_api.dart'; import 'package:frosty/core/auth/auth_store.dart'; import 'package:frosty/models/category.dart'; import 'package:frosty/screens/home/stores/list_store.dart'; @@ -20,6 +21,7 @@ class CategoryCard extends StatelessWidget { MaterialPageRoute( builder: (context) => CategoryStreams( store: ListStore( + twitchApi: context.read(), authStore: context.read(), listType: ListType.category, categoryInfo: category, diff --git a/lib/screens/home/widgets/stream_card.dart b/lib/screens/home/widgets/stream_card.dart index d3b38da9..2d3c52de 100644 --- a/lib/screens/home/widgets/stream_card.dart +++ b/lib/screens/home/widgets/stream_card.dart @@ -1,7 +1,13 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.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/core/auth/auth_store.dart'; import 'package:frosty/models/stream.dart'; +import 'package:frosty/screens/channel/stores/chat_assets_store.dart'; +import 'package:frosty/screens/channel/stores/chat_details_store.dart'; import 'package:frosty/screens/channel/stores/chat_store.dart'; import 'package:frosty/screens/channel/video_chat.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; @@ -37,6 +43,15 @@ class StreamCard extends StatelessWidget { channelName: streamInfo.userLogin, auth: context.read(), settings: context.read(), + chatDetailsStore: ChatDetailsStore( + twitchApi: context.read(), + ), + assetsStore: ChatAssetsStore( + twitchApi: context.read(), + ffzApi: context.read(), + bttvApi: context.read(), + sevenTvApi: context.read(), + ), ), ), ), diff --git a/lib/widgets/profile_picture.dart b/lib/widgets/profile_picture.dart index d9e52bde..f437f968 100644 --- a/lib/widgets/profile_picture.dart +++ b/lib/widgets/profile_picture.dart @@ -18,7 +18,7 @@ class ProfilePicture extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder( - future: Twitch.getUser(userLogin: userLogin, headers: context.read().headersTwitch), + future: context.read().getUser(userLogin: userLogin, headers: context.read().headersTwitch), builder: (context, AsyncSnapshot snapshot) { return CircleAvatar( radius: radius,