Add ability to pin channels in "Following" tab (#395)
* add pinned channels list global config setting * add ability to pin streams * ensure that all pinned streams are fetched * rename * tweak padding * fix typo
This commit is contained in:
parent
0d503935df
commit
3a4cd449f7
11 changed files with 380 additions and 81 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ 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';
|
||||
|
||||
|
|
@ -19,12 +19,16 @@ 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
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@ mixin _$ListStore on ListStoreBase, Store {
|
|||
bool get hasMore => (_$hasMoreComputed ??=
|
||||
Computed<bool>(() => super.hasMore, name: 'ListStoreBase.hasMore'))
|
||||
.value;
|
||||
Computed<bool>? _$isLoadingComputed;
|
||||
|
||||
@override
|
||||
bool get isLoading =>
|
||||
(_$isLoadingComputed ??= Computed<bool>(() => super.isLoading,
|
||||
name: 'ListStoreBase.isLoading'))
|
||||
.value;
|
||||
Computed<ObservableList<StreamTwitch>>? _$streamsComputed;
|
||||
|
||||
@override
|
||||
|
|
@ -23,24 +30,6 @@ mixin _$ListStore on ListStoreBase, Store {
|
|||
name: 'ListStoreBase.streams'))
|
||||
.value;
|
||||
|
||||
late final _$_isLoadingAtom =
|
||||
Atom(name: 'ListStoreBase._isLoading', context: context);
|
||||
|
||||
bool get isLoading {
|
||||
_$_isLoadingAtom.reportRead();
|
||||
return super._isLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _isLoading => isLoading;
|
||||
|
||||
@override
|
||||
set _isLoading(bool value) {
|
||||
_$_isLoadingAtom.reportWrite(value, super._isLoading, () {
|
||||
super._isLoading = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_allStreamsAtom =
|
||||
Atom(name: 'ListStoreBase._allStreams', context: context);
|
||||
|
||||
|
|
@ -59,6 +48,62 @@ mixin _$ListStore on ListStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_isAllStreamsLoadingAtom =
|
||||
Atom(name: 'ListStoreBase._isAllStreamsLoading', context: context);
|
||||
|
||||
bool get isAllStreamsLoading {
|
||||
_$_isAllStreamsLoadingAtom.reportRead();
|
||||
return super._isAllStreamsLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _isAllStreamsLoading => isAllStreamsLoading;
|
||||
|
||||
@override
|
||||
set _isAllStreamsLoading(bool value) {
|
||||
_$_isAllStreamsLoadingAtom.reportWrite(value, super._isAllStreamsLoading,
|
||||
() {
|
||||
super._isAllStreamsLoading = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_pinnedStreamsAtom =
|
||||
Atom(name: 'ListStoreBase._pinnedStreams', context: context);
|
||||
|
||||
ObservableList<StreamTwitch> get pinnedStreams {
|
||||
_$_pinnedStreamsAtom.reportRead();
|
||||
return super._pinnedStreams;
|
||||
}
|
||||
|
||||
@override
|
||||
ObservableList<StreamTwitch> get _pinnedStreams => pinnedStreams;
|
||||
|
||||
@override
|
||||
set _pinnedStreams(ObservableList<StreamTwitch> value) {
|
||||
_$_pinnedStreamsAtom.reportWrite(value, super._pinnedStreams, () {
|
||||
super._pinnedStreams = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_isPinnedStreamsLoadingAtom =
|
||||
Atom(name: 'ListStoreBase._isPinnedStreamsLoading', context: context);
|
||||
|
||||
bool get isPinnedStreamsLoading {
|
||||
_$_isPinnedStreamsLoadingAtom.reportRead();
|
||||
return super._isPinnedStreamsLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _isPinnedStreamsLoading => isPinnedStreamsLoading;
|
||||
|
||||
@override
|
||||
set _isPinnedStreamsLoading(bool value) {
|
||||
_$_isPinnedStreamsLoadingAtom
|
||||
.reportWrite(value, super._isPinnedStreamsLoading, () {
|
||||
super._isPinnedStreamsLoading = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_categoryDetailsAtom =
|
||||
Atom(name: 'ListStoreBase._categoryDetails', context: context);
|
||||
|
||||
|
|
@ -77,6 +122,25 @@ mixin _$ListStore on ListStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_isCategoryDetailsLoadingAtom =
|
||||
Atom(name: 'ListStoreBase._isCategoryDetailsLoading', context: context);
|
||||
|
||||
bool get isCategoryDetailsLoading {
|
||||
_$_isCategoryDetailsLoadingAtom.reportRead();
|
||||
return super._isCategoryDetailsLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _isCategoryDetailsLoading => isCategoryDetailsLoading;
|
||||
|
||||
@override
|
||||
set _isCategoryDetailsLoading(bool value) {
|
||||
_$_isCategoryDetailsLoadingAtom
|
||||
.reportWrite(value, super._isCategoryDetailsLoading, () {
|
||||
super._isCategoryDetailsLoading = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$showJumpButtonAtom =
|
||||
Atom(name: 'ListStoreBase.showJumpButton', context: context);
|
||||
|
||||
|
|
@ -119,6 +183,22 @@ mixin _$ListStore on ListStoreBase, Store {
|
|||
return _$getStreamsAsyncAction.run(() => super.getStreams());
|
||||
}
|
||||
|
||||
late final _$getPinnedStreamsAsyncAction =
|
||||
AsyncAction('ListStoreBase.getPinnedStreams', context: context);
|
||||
|
||||
@override
|
||||
Future<void> getPinnedStreams() {
|
||||
return _$getPinnedStreamsAsyncAction.run(() => super.getPinnedStreams());
|
||||
}
|
||||
|
||||
late final _$refreshStreamsAsyncAction =
|
||||
AsyncAction('ListStoreBase.refreshStreams', context: context);
|
||||
|
||||
@override
|
||||
Future<void> refreshStreams() {
|
||||
return _$refreshStreamsAsyncAction.run(() => super.refreshStreams());
|
||||
}
|
||||
|
||||
late final _$_getCategoryDetailsAsyncAction =
|
||||
AsyncAction('ListStoreBase._getCategoryDetails', context: context);
|
||||
|
||||
|
|
@ -128,25 +208,12 @@ mixin _$ListStore on ListStoreBase, Store {
|
|||
.run(() => super._getCategoryDetails());
|
||||
}
|
||||
|
||||
late final _$ListStoreBaseActionController =
|
||||
ActionController(name: 'ListStoreBase', context: context);
|
||||
|
||||
@override
|
||||
Future<void> refreshStreams() {
|
||||
final _$actionInfo = _$ListStoreBaseActionController.startAction(
|
||||
name: 'ListStoreBase.refreshStreams');
|
||||
try {
|
||||
return super.refreshStreams();
|
||||
} finally {
|
||||
_$ListStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
showJumpButton: ${showJumpButton},
|
||||
hasMore: ${hasMore},
|
||||
isLoading: ${isLoading},
|
||||
streams: ${streams}
|
||||
''';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:frosty/widgets/alert_message.dart';
|
|||
import 'package:frosty/widgets/animated_scroll_border.dart';
|
||||
import 'package:frosty/widgets/loading_indicator.dart';
|
||||
import 'package:frosty/widgets/scroll_to_top_button.dart';
|
||||
import 'package:frosty/widgets/section_header.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// A widget that displays a list of followed or top streams based on the provided [listType].
|
||||
|
|
@ -43,6 +44,7 @@ class _StreamsListState extends State<StreamsList>
|
|||
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
|
||||
late final _listStore = ListStore(
|
||||
authStore: context.read<AuthStore>(),
|
||||
settingsStore: context.read<SettingsStore>(),
|
||||
twitchApi: context.read<TwitchApi>(),
|
||||
listType: widget.listType,
|
||||
categoryId: widget.categoryId,
|
||||
|
|
@ -126,6 +128,17 @@ class _StreamsListState extends State<StreamsList>
|
|||
);
|
||||
}
|
||||
|
||||
final settingsStore = context.watch<SettingsStore>();
|
||||
|
||||
final unpinnedStreams = _listStore.streams
|
||||
.where(
|
||||
(stream) =>
|
||||
!settingsStore.pinnedChannelIds.contains(stream.userId),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final isFollowingTab = widget.listType == ListType.followed;
|
||||
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
|
|
@ -144,42 +157,113 @@ class _StreamsListState extends State<StreamsList>
|
|||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: _listStore.scrollController,
|
||||
child: ListView.builder(
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
controller: _listStore.scrollController,
|
||||
itemCount: _listStore.streams.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > _listStore.streams.length - 10 &&
|
||||
_listStore.hasMore) {
|
||||
debugPrint('$index ${_listStore.streams.length}');
|
||||
|
||||
_listStore.getStreams();
|
||||
}
|
||||
return Observer(
|
||||
builder: (context) =>
|
||||
context.read<SettingsStore>().largeStreamCard
|
||||
slivers: [
|
||||
if (isFollowingTab &&
|
||||
_listStore.pinnedStreams.isNotEmpty) ...[
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'Pinnned',
|
||||
isFirst: true,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
8,
|
||||
16,
|
||||
8,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: _listStore.pinnedStreams.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return settingsStore.largeStreamCard
|
||||
? LargeStreamCard(
|
||||
key: ValueKey(
|
||||
_listStore
|
||||
.pinnedStreams[index].userId,
|
||||
),
|
||||
streamInfo:
|
||||
_listStore.pinnedStreams[index],
|
||||
showThumbnail: context
|
||||
.read<SettingsStore>()
|
||||
.showThumbnails,
|
||||
showCategory:
|
||||
widget.categoryId == null,
|
||||
showPinOption: true,
|
||||
isPinned: true,
|
||||
)
|
||||
: StreamCard(
|
||||
key: ValueKey(
|
||||
_listStore
|
||||
.pinnedStreams[index].userId,
|
||||
),
|
||||
streamInfo:
|
||||
_listStore.pinnedStreams[index],
|
||||
showThumbnail: context
|
||||
.read<SettingsStore>()
|
||||
.showThumbnails,
|
||||
showCategory:
|
||||
widget.categoryId == null,
|
||||
showPinOption: true,
|
||||
isPinned: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SectionHeader(
|
||||
'All',
|
||||
isFirst: true,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
8,
|
||||
16,
|
||||
8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SliverList.builder(
|
||||
itemCount: unpinnedStreams.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > unpinnedStreams.length - 10 &&
|
||||
_listStore.hasMore) {
|
||||
_listStore.getStreams();
|
||||
}
|
||||
return Observer(
|
||||
builder: (context) => settingsStore
|
||||
.largeStreamCard
|
||||
? LargeStreamCard(
|
||||
key: ValueKey(
|
||||
_listStore.streams[index].userId,
|
||||
unpinnedStreams[index].userId,
|
||||
),
|
||||
streamInfo: _listStore.streams[index],
|
||||
showThumbnail: context
|
||||
.read<SettingsStore>()
|
||||
.showThumbnails,
|
||||
streamInfo: unpinnedStreams[index],
|
||||
showThumbnail:
|
||||
settingsStore.showThumbnails,
|
||||
showCategory: widget.categoryId == null,
|
||||
showPinOption: isFollowingTab,
|
||||
isPinned: false,
|
||||
)
|
||||
: StreamCard(
|
||||
key: ValueKey(
|
||||
_listStore.streams[index].userId,
|
||||
unpinnedStreams[index].userId,
|
||||
),
|
||||
streamInfo: _listStore.streams[index],
|
||||
showThumbnail: context
|
||||
.read<SettingsStore>()
|
||||
.showThumbnails,
|
||||
streamInfo: unpinnedStreams[index],
|
||||
showThumbnail:
|
||||
settingsStore.showThumbnails,
|
||||
showCategory: widget.categoryId == null,
|
||||
showPinOption: isFollowingTab,
|
||||
isPinned: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -360,6 +360,7 @@ abstract class _SettingsStoreBase with Store {
|
|||
// * Global configs
|
||||
static const defaultFullScreen = false;
|
||||
static const defaultFullScreenChatOverlay = false;
|
||||
static const defaultPinnedChannelIds = <String>[];
|
||||
|
||||
@JsonKey(defaultValue: defaultFullScreen)
|
||||
@observable
|
||||
|
|
@ -369,10 +370,15 @@ abstract class _SettingsStoreBase with Store {
|
|||
@observable
|
||||
var fullScreenChatOverlay = defaultFullScreenChatOverlay;
|
||||
|
||||
@JsonKey(defaultValue: defaultPinnedChannelIds)
|
||||
@observable
|
||||
var pinnedChannelIds = defaultPinnedChannelIds;
|
||||
|
||||
@action
|
||||
void resetGlobalConfigs() {
|
||||
fullScreen = defaultFullScreen;
|
||||
fullScreenChatOverlay = defaultFullScreenChatOverlay;
|
||||
pinnedChannelIds = defaultPinnedChannelIds;
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
|||
|
|
@ -67,7 +67,11 @@ SettingsStore _$SettingsStoreFromJson(Map<String, dynamic> json) =>
|
|||
..shareCrashLogsAndAnalytics =
|
||||
json['shareCrashLogsAndAnalytics'] as bool? ?? true
|
||||
..fullScreen = json['fullScreen'] as bool? ?? false
|
||||
..fullScreenChatOverlay = json['fullScreenChatOverlay'] as bool? ?? false;
|
||||
..fullScreenChatOverlay = json['fullScreenChatOverlay'] as bool? ?? false
|
||||
..pinnedChannelIds = (json['pinnedChannelIds'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
||||
<String, dynamic>{
|
||||
|
|
@ -118,6 +122,7 @@ Map<String, dynamic> _$SettingsStoreToJson(SettingsStore instance) =>
|
|||
'shareCrashLogsAndAnalytics': instance.shareCrashLogsAndAnalytics,
|
||||
'fullScreen': instance.fullScreen,
|
||||
'fullScreenChatOverlay': instance.fullScreenChatOverlay,
|
||||
'pinnedChannelIds': instance.pinnedChannelIds,
|
||||
};
|
||||
|
||||
const _$ThemeTypeEnumMap = {
|
||||
|
|
@ -895,6 +900,22 @@ mixin _$SettingsStore on _SettingsStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$pinnedChannelIdsAtom =
|
||||
Atom(name: '_SettingsStoreBase.pinnedChannelIds', context: context);
|
||||
|
||||
@override
|
||||
List<String> get pinnedChannelIds {
|
||||
_$pinnedChannelIdsAtom.reportRead();
|
||||
return super.pinnedChannelIds;
|
||||
}
|
||||
|
||||
@override
|
||||
set pinnedChannelIds(List<String> value) {
|
||||
_$pinnedChannelIdsAtom.reportWrite(value, super.pinnedChannelIds, () {
|
||||
super.pinnedChannelIds = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_SettingsStoreBaseActionController =
|
||||
ActionController(name: '_SettingsStoreBase', context: context);
|
||||
|
||||
|
|
@ -1012,7 +1033,8 @@ showRecentMessages: ${showRecentMessages},
|
|||
darkenRecentMessages: ${darkenRecentMessages},
|
||||
shareCrashLogsAndAnalytics: ${shareCrashLogsAndAnalytics},
|
||||
fullScreen: ${fullScreen},
|
||||
fullScreenChatOverlay: ${fullScreenChatOverlay}
|
||||
fullScreenChatOverlay: ${fullScreenChatOverlay},
|
||||
pinnedChannelIds: ${pinnedChannelIds}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:frosty/screens/settings/stores/auth_store.dart';
|
||||
import 'package:frosty/screens/settings/stores/settings_store.dart';
|
||||
import 'package:frosty/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class BlockReportModal extends StatelessWidget {
|
||||
class UserActionsModal extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
final String name;
|
||||
final String userLogin;
|
||||
final String userId;
|
||||
final bool showPinOption;
|
||||
final bool? isPinned;
|
||||
|
||||
const BlockReportModal({
|
||||
const UserActionsModal({
|
||||
super.key,
|
||||
required this.authStore,
|
||||
required this.name,
|
||||
required this.userLogin,
|
||||
required this.userId,
|
||||
this.showPinOption = false,
|
||||
this.isPinned,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -23,6 +29,26 @@ class BlockReportModal extends StatelessWidget {
|
|||
primary: false,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
if (showPinOption)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.push_pin_outlined),
|
||||
title: Text('${isPinned == true ? 'Unpin' : 'Pin'} $name'),
|
||||
onTap: () {
|
||||
if (isPinned == true) {
|
||||
context.read<SettingsStore>().pinnedChannelIds = [
|
||||
...context.read<SettingsStore>().pinnedChannelIds
|
||||
..remove(userId),
|
||||
];
|
||||
} else {
|
||||
context.read<SettingsStore>().pinnedChannelIds = [
|
||||
...context.read<SettingsStore>().pinnedChannelIds,
|
||||
userId,
|
||||
];
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (authStore.isLoggedIn)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.block_rounded),
|
||||
Loading…
Add table
Add a link
Reference in a new issue