Refactor APIs to use a persistent client

This commit is contained in:
tommyxchow 2022-01-14 14:56:54 -05:00
parent 91d70fffae
commit 34263fbf94
19 changed files with 256 additions and 127 deletions

View file

@ -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<List<Emote>> getEmotesGlobal() async {
Future<List<Emote>> 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<List<Emote>> getEmotesChannel({required String id}) async {
Future<List<Emote>> 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<Map<String, Badge>?> getBadges() async {
Future<Map<String, Badge>?> 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;

View file

@ -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<List<Emote>> getEmotesGlobal() async {
Future<List<Emote>> 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<Tuple2<RoomFFZ, List<Emote>>?> getRoomInfo({required String name}) async {
Future<Tuple2<RoomFFZ, List<Emote>>?> 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<Map<String, List<Badge>>?> getBadges() async {
Future<Map<String, List<Badge>>?> 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);

View file

@ -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<List<Emote>> getEmotesGlobal() async {
Future<List<Emote>> 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<List<Emote>> getEmotesChannel({required String user}) async {
Future<List<Emote>> 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<Map<String, List<Badge>>?> getBadges() async {
Future<Map<String, List<Badge>>?> 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;

View file

@ -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<List<Emote>> getEmotesGlobal({required Map<String, String>? headers}) async {
class TwitchApi {
final Client _client;
TwitchApi(this._client);
Future<List<Emote>> getEmotesGlobal({required Map<String, String>? 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<List<Emote>> getEmotesChannel({required String id, required Map<String, String>? headers}) async {
Future<List<Emote>> getEmotesChannel({required String id, required Map<String, String>? 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<List<Emote>> getEmotesSets({required String setId, required Map<String, String>? headers, sub = false}) async {
Future<List<Emote>> getEmotesSets({required String setId, required Map<String, String>? 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<Map<String, Badge>> getBadgesGlobal() async {
Future<Map<String, Badge>> 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 = <String, Badge>{};
@ -89,9 +93,9 @@ class Twitch {
}
/// Returns a map of a channel's Twitch badges to their URL.
static Future<Map<String, Badge>> getBadgesChannel({required String id}) async {
Future<Map<String, Badge>> 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 = <String, Badge>{};
@ -109,8 +113,8 @@ class Twitch {
}
/// Returns the user's info given their token (headers)
static Future<UserTwitch?> getUserInfo({required Map<String, String>? headers}) async {
final response = await http.get(Uri.parse('https://api.twitch.tv/helix/users'), headers: headers);
Future<UserTwitch?> getUserInfo({required Map<String, String>? 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<String?> getDefaultToken() async {
Future<String?> 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<bool> validateToken({required String token}) async {
Future<bool> 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<StreamsTwitch?> getTopStreams({required Map<String, String>? headers, String? cursor}) async {
Future<StreamsTwitch?> getTopStreams({required Map<String, String>? 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<StreamsTwitch?> getFollowedStreams({
Future<StreamsTwitch?> getFollowedStreams({
required String id,
required Map<String, String>? 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<StreamsTwitch?> getStreamsUnderGame({
Future<StreamsTwitch?> getStreamsUnderGame({
required String gameId,
required Map<String, String>? 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<StreamTwitch?> getStream({required String userLogin, required Map<String, String>? headers}) async {
Future<StreamTwitch?> getStream({required String userLogin, required Map<String, String>? 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<UserTwitch?> getUser({String? userLogin, String? id, required Map<String, String>? headers}) async {
Future<UserTwitch?> getUser({String? userLogin, String? id, required Map<String, String>? 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<Channel?> getChannel({required String userId, required Map<String, String>? headers}) async {
final response = await http.get(Uri.parse('https://api.twitch.tv/helix/channels?broadcaster_id=$userId'), headers: headers);
Future<Channel?> getChannel({required String userId, required Map<String, String>? 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<List<ChannelQuery>> searchChannels({required String query, required Map<String, String>? headers}) async {
final response = await http.get(Uri.parse('https://api.twitch.tv/helix/search/channels?first=8&query=$query'), headers: headers);
Future<List<ChannelQuery>> searchChannels({required String query, required Map<String, String>? 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<CategoriesTwitch?> getTopGames({required Map<String, String>? headers, String? cursor}) async {
Future<CategoriesTwitch?> getTopGames({required Map<String, String>? 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<CategoriesTwitch?> searchCategories({required Map<String, String>? headers, required String query, String? cursor}) async {
Future<CategoriesTwitch?> searchCategories({required Map<String, String>? 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<int?> getSubscriberCount({required String userId, required Map<String, String>? headers}) async {
Future<int?> getSubscriberCount({required String userId, required Map<String, String>? 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<ChatUsers?> getChatters({required String userLogin}) async {
Future<ChatUsers?> 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<List<UserBlockedTwitch>> getUserBlockedList({
Future<List<UserBlockedTwitch>> getUserBlockedList({
required String id,
required Map<String, String>? 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<bool> blockUser({required String userId, required Map<String, String> headers}) async {
Future<bool> blockUser({required String userId, required Map<String, String> 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<bool> unblockUser({required String userId, required Map<String, String> headers}) async {
Future<bool> unblockUser({required String userId, required Map<String, String> 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 {

View file

@ -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<String, String> 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<void> 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!);
}
}

View file

@ -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<void> init({required Map<String, String> 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<void> block({required String targetId, required Map<String, String> 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<void> unblock({required String targetId, required Map<String, String> 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<void> refreshBlockedUsers({required Map<String, String> 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() {

View file

@ -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<void> 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<void> 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<AuthStore>(create: (_) => authStore),
Provider<SettingsStore>(create: (_) => settingsStore),
Provider<TwitchApi>(create: (_) => twitchApiService),
Provider<BTTVApi>(create: (_) => bttvApiService),
Provider<FFZApi>(create: (_) => ffzApiService),
Provider<SevenTVAPI>(create: (_) => sevenTvApiService),
],
child: const MyApp(),
),
@ -54,9 +70,6 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final authStore = context.read<AuthStore>();
final settingsStore = context.read<SettingsStore>();
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<AuthStore>();
final settingsStore = context.read<SettingsStore>();
final twitchApi = context.read<TwitchApi>();
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,
),
),
);
},

View file

@ -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<void> getAssets({required String channelName, required Map<String, String> 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<void> getEmotes({required UserTwitch channelInfo, required Map<String, String> 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 ?? <Emote>[];
}),
@ -107,17 +119,18 @@ abstract class _ChatAssetsStoreBase with Store {
@action
Future<void> getBadges({required UserTwitch channelInfo, required Map<String, String> 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<void> getUserEmotes({required List<String> emoteSets, required Map<String, String> 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());
}

View file

@ -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<void> updateChatters(String userLogin) async => _chatUsers = await Twitch.getChatters(userLogin: userLogin);
Future<void> updateChatters(String userLogin) async => _chatUsers = await twitchApi.getChatters(userLogin: userLogin);
}

View file

@ -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,
}) {

View file

@ -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<void> 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;
}

View file

@ -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<VideoChat> {
key: GlobalKey(),
userLogin: chatStore.channelName,
videoStore: VideoStore(
twitchApi: context.read<TwitchApi>(),
userLogin: chatStore.channelName,
authStore: chatStore.auth,
settingsStore: chatStore.settings,

View file

@ -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<TwitchApi>(),
),
assetsStore: ChatAssetsStore(
twitchApi: context.read<TwitchApi>(),
ffzApi: context.read<FFZApi>(),
bttvApi: context.read<BTTVApi>(),
sevenTvApi: context.read<SevenTVAPI>(),
),
channelName: channelInfo.broadcasterLogin,
auth: searchStore.authStore,
settings: context.read<SettingsStore>(),
@ -93,6 +108,15 @@ class SearchResultsChannels extends StatelessWidget {
builder: (context) => VideoChat(
displayName: channel.displayName,
chatStore: ChatStore(
chatDetailsStore: ChatDetailsStore(
twitchApi: context.read<TwitchApi>(),
),
assetsStore: ChatAssetsStore(
twitchApi: context.read<TwitchApi>(),
ffzApi: context.read<FFZApi>(),
bttvApi: context.read<BTTVApi>(),
sevenTvApi: context.read<SevenTVAPI>(),
),
channelName: channel.broadcasterLogin,
auth: searchStore.authStore,
settings: context.read<SettingsStore>(),

View file

@ -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<void> 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();

View file

@ -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,

View file

@ -13,6 +13,8 @@ class SearchStore = _SearchStoreBase with _$SearchStore;
abstract class _SearchStoreBase with Store {
final AuthStore authStore;
final TwitchApi twitchApi;
@readonly
var _searchHistory = ObservableList<String>();
@ -22,7 +24,7 @@ abstract class _SearchStoreBase with Store {
@readonly
ObservableFuture<CategoriesTwitch?>? _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<Channel?> 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;

View file

@ -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<TwitchApi>(),
authStore: context.read<AuthStore>(),
listType: ListType.category,
categoryInfo: category,

View file

@ -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<AuthStore>(),
settings: context.read<SettingsStore>(),
chatDetailsStore: ChatDetailsStore(
twitchApi: context.read<TwitchApi>(),
),
assetsStore: ChatAssetsStore(
twitchApi: context.read<TwitchApi>(),
ffzApi: context.read<FFZApi>(),
bttvApi: context.read<BTTVApi>(),
sevenTvApi: context.read<SevenTVAPI>(),
),
),
),
),

View file

@ -18,7 +18,7 @@ class ProfilePicture extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Twitch.getUser(userLogin: userLogin, headers: context.read<AuthStore>().headersTwitch),
future: context.read<TwitchApi>().getUser(userLogin: userLogin, headers: context.read<AuthStore>().headersTwitch),
builder: (context, AsyncSnapshot<UserTwitch?> snapshot) {
return CircleAvatar(
radius: radius,