Skip to content

Commit

Permalink
Add ability to pin channels in "Following" tab (#395)
Browse files Browse the repository at this point in the history
* add pinned channels list global config setting

* add ability to pin streams

* ensure that all pinned streams are fetched

* rename

* tweak padding

* fix typo
  • Loading branch information
tommyxchow authored Sep 10, 2024
1 parent 0d50393 commit 3a4cd44
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 81 deletions.
17 changes: 17 additions & 0 deletions lib/apis/twitch_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,23 @@ class TwitchApi {
}
}

Future<StreamsTwitch> getStreamsByIds({
required List<String> userIds,
required Map<String, String> headers,
}) async {
final uri = Uri.parse(
'https://api.twitch.tv/helix/streams?${userIds.map((e) => 'user_id=$e').join('&')}&first=100',
);

final response = await _client.get(uri, headers: headers);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
return StreamsTwitch.fromJson(decoded);
} else {
return Future.error('Failed to get stream info');
}
}

/// Returns a [UserTwitch] object containing the user info associated with the given [userLogin].
Future<UserTwitch> getUser({
String? userLogin,
Expand Down
4 changes: 2 additions & 2 deletions lib/screens/channel/chat/widgets/chat_user_modal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart';
import 'package:frosty/screens/channel/chat/widgets/chat_message.dart';
import 'package:frosty/utils.dart';
import 'package:frosty/widgets/alert_message.dart';
import 'package:frosty/widgets/block_report_modal.dart';
import 'package:frosty/widgets/profile_picture.dart';
import 'package:frosty/widgets/user_actions_modal.dart';

class ChatUserModal extends StatefulWidget {
final ChatStore chatStore;
Expand Down Expand Up @@ -72,7 +72,7 @@ class _ChatUserModalState extends State<ChatUserModal> {
tooltip: 'More',
onPressed: () => showModalBottomSheet(
context: context,
builder: (context) => BlockReportModal(
builder: (context) => UserActionsModal(
authStore: widget.chatStore.auth,
name: name,
userLogin: widget.username,
Expand Down
4 changes: 2 additions & 2 deletions lib/screens/home/search/search_results_channels.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import 'package:frosty/screens/channel/channel.dart';
import 'package:frosty/screens/home/search/search_store.dart';
import 'package:frosty/utils.dart';
import 'package:frosty/widgets/alert_message.dart';
import 'package:frosty/widgets/block_report_modal.dart';
import 'package:frosty/widgets/loading_indicator.dart';
import 'package:frosty/widgets/profile_picture.dart';
import 'package:frosty/widgets/uptime.dart';
import 'package:frosty/widgets/user_actions_modal.dart';
import 'package:mobx/mobx.dart';

class SearchResultsChannels extends StatefulWidget {
Expand Down Expand Up @@ -107,7 +107,7 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {

showModalBottomSheet(
context: context,
builder: (context) => BlockReportModal(
builder: (context) => UserActionsModal(
authStore: widget.searchStore.authStore,
name: displayName,
userLogin: channel.broadcasterLogin,
Expand Down
10 changes: 8 additions & 2 deletions lib/screens/home/stream_list/large_stream_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@ import 'package:frosty/screens/channel/video/video_bar.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/theme.dart';
import 'package:frosty/utils.dart';
import 'package:frosty/widgets/block_report_modal.dart';
import 'package:frosty/widgets/cached_image.dart';
import 'package:frosty/widgets/loading_indicator.dart';
import 'package:frosty/widgets/uptime.dart';
import 'package:frosty/widgets/user_actions_modal.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';

class LargeStreamCard extends StatelessWidget {
final StreamTwitch streamInfo;
final bool showThumbnail;
final bool showCategory;
final bool showPinOption;
final bool? isPinned;

const LargeStreamCard({
super.key,
required this.streamInfo,
required this.showThumbnail,
this.showCategory = true,
this.showPinOption = false,
this.isPinned,
});

@override
Expand Down Expand Up @@ -151,11 +155,13 @@ class LargeStreamCard extends StatelessWidget {

showModalBottomSheet(
context: context,
builder: (context) => BlockReportModal(
builder: (context) => UserActionsModal(
authStore: context.read<AuthStore>(),
name: streamerName,
userLogin: streamInfo.userLogin,
userId: streamInfo.userId,
showPinOption: showPinOption,
isPinned: isPinned,
),
);
},
Expand Down
10 changes: 8 additions & 2 deletions lib/screens/home/stream_list/stream_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import 'package:frosty/screens/home/top/categories/category_streams.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/theme.dart';
import 'package:frosty/utils.dart';
import 'package:frosty/widgets/block_report_modal.dart';
import 'package:frosty/widgets/cached_image.dart';
import 'package:frosty/widgets/loading_indicator.dart';
import 'package:frosty/widgets/photo_view.dart';
import 'package:frosty/widgets/profile_picture.dart';
import 'package:frosty/widgets/uptime.dart';
import 'package:frosty/widgets/user_actions_modal.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';

Expand All @@ -22,12 +22,16 @@ class StreamCard extends StatelessWidget {
final StreamTwitch streamInfo;
final bool showThumbnail;
final bool showCategory;
final bool showPinOption;
final bool? isPinned;

const StreamCard({
super.key,
required this.streamInfo,
required this.showThumbnail,
this.showCategory = true,
this.showPinOption = false,
this.isPinned,
});

@override
Expand Down Expand Up @@ -210,11 +214,13 @@ class StreamCard extends StatelessWidget {

showModalBottomSheet(
context: context,
builder: (context) => BlockReportModal(
builder: (context) => UserActionsModal(
authStore: context.read<AuthStore>(),
name: streamerName,
userLogin: streamInfo.userLogin,
userId: streamInfo.userId,
showPinOption: showPinOption,
isPinned: isPinned,
),
);
},
Expand Down
91 changes: 78 additions & 13 deletions lib/screens/home/stream_list/stream_list_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:frosty/apis/twitch_api.dart';
import 'package:frosty/models/category.dart';
import 'package:frosty/models/stream.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:mobx/mobx.dart';

part 'stream_list_store.g.dart';
Expand All @@ -15,6 +16,8 @@ abstract class ListStoreBase with Store {
/// The authentication store.
final AuthStore authStore;

final SettingsStore settingsStore;

/// Twitch API service class for making requests.
final TwitchApi twitchApi;

Expand All @@ -36,19 +39,35 @@ abstract class ListStoreBase with Store {

/// Returns whether or not there are more streams and loading status for pagination.
@computed
bool get hasMore => _isLoading == false && _streamsCursor != null;
bool get hasMore => isLoading == false && _streamsCursor != null;

@computed
bool get isLoading =>
_isAllStreamsLoading ||
_isPinnedStreamsLoading ||
_isCategoryDetailsLoading;

/// The loading status for pagination.
@readonly
bool _isLoading = false;
/// The list of the fetched streams.
@readonly
var _allStreams = ObservableList<StreamTwitch>();

@readonly
bool _isAllStreamsLoading = false;

@readonly
var _pinnedStreams = ObservableList<StreamTwitch>();

@readonly
var _isPinnedStreamsLoading = false;

@readonly
CategoryTwitch? _categoryDetails;

@readonly
var _isCategoryDetailsLoading = false;

/// Whether or not the scroll to top button is visible.
@observable
var showJumpButton = false;
Expand All @@ -68,8 +87,11 @@ abstract class ListStoreBase with Store {
@readonly
String? _error;

ReactionDisposer? _pinnedStreamsReactioniDisposer;

ListStoreBase({
required this.authStore,
required this.settingsStore,
required this.twitchApi,
required this.listType,
this.categoryId,
Expand All @@ -86,17 +108,26 @@ abstract class ListStoreBase with Store {
});
}

getStreams();
if (listType == ListType.followed) {
_pinnedStreamsReactioniDisposer = reaction(
(_) => settingsStore.pinnedChannelIds,
(_) => getPinnedStreams(),
);

if (listType == ListType.category && categoryId != null) {
getPinnedStreams();
}

if (listType == ListType.category) {
_getCategoryDetails();
}

getStreams();
}

/// Fetches the streams based on the type and current cursor.
@action
Future<void> getStreams() async {
_isLoading = true;
_isAllStreamsLoading = true;

try {
final StreamsTwitch newStreams;
Expand Down Expand Up @@ -137,20 +168,50 @@ abstract class ListStoreBase with Store {
_error = e.toString();
}

_isLoading = false;
_isAllStreamsLoading = false;
}

@action
Future<void> getPinnedStreams() async {
if (settingsStore.pinnedChannelIds.isEmpty) {
_pinnedStreams.clear();
return;
}

_isPinnedStreamsLoading = true;

try {
_pinnedStreams = (await twitchApi.getStreamsByIds(
userIds: settingsStore.pinnedChannelIds,
headers: authStore.headersTwitch,
))
.data
.asObservable();

_error = null;
} on SocketException {
_error = 'Failed to connect';
} catch (e) {
_error = e.toString();
}

_isPinnedStreamsLoading = false;
}

/// Resets the cursor and then fetches the streams.
@action
Future<void> refreshStreams() {
_streamsCursor = null;
Future<void> refreshStreams() async {
if (listType == ListType.followed) await getPinnedStreams();

return getStreams();
_streamsCursor = null;
await getStreams();
}

@action
Future<void> _getCategoryDetails() async {
_isLoading = true;
if (categoryId == null) return;

_isCategoryDetailsLoading = true;

final categoryDetails = await twitchApi.getCategory(
headers: authStore.headersTwitch,
Expand All @@ -159,7 +220,7 @@ abstract class ListStoreBase with Store {

_categoryDetails = categoryDetails.data.first;

_isLoading = false;
_isCategoryDetailsLoading = false;
}

/// Checks the last time the streams were refreshed and updates them if it has been more than 5 minutes.
Expand All @@ -172,7 +233,11 @@ abstract class ListStoreBase with Store {
lastTimeRefreshed = now;
}

void dispose() => scrollController?.dispose();
void dispose() {
_pinnedStreamsReactioniDisposer?.call();

scrollController?.dispose();
}
}

/// The possible types of lists that can be displayed.
Expand Down
Loading

0 comments on commit 3a4cd44

Please sign in to comment.