import 'package:fluent_reader_lite/components/badge.dart';
import 'package:fluent_reader_lite/components/mark_all_action_sheet.dart';
import 'package:fluent_reader_lite/components/my_list_tile.dart';
import 'package:fluent_reader_lite/components/responsive_action_sheet.dart';
import 'package:fluent_reader_lite/components/subscription_item.dart';
import 'package:fluent_reader_lite/components/sync_control.dart';
import 'package:fluent_reader_lite/generated/l10n.dart';
import 'package:fluent_reader_lite/models/source.dart';
import 'package:fluent_reader_lite/models/sources_model.dart';
import 'package:fluent_reader_lite/models/sync_model.dart';
import 'package:fluent_reader_lite/pages/group_list_page.dart';
import 'package:fluent_reader_lite/pages/home_page.dart';
import 'package:fluent_reader_lite/pages/settings/text_editor_page.dart';
import 'package:fluent_reader_lite/utils/colors.dart';
import 'package:fluent_reader_lite/utils/global.dart';
import 'package:fluent_reader_lite/utils/store.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' hide Badge;
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:provider/provider.dart';
enum SubscriptionSortType {
ByLatest,
ByNameAsc,
ByNameDesc,
ByUnread,
}
class SubscriptionListPage extends StatefulWidget {
final ScrollTopNotifier scrollTopNotifier;
SubscriptionListPage(this.scrollTopNotifier, {Key? key}) : super(key: key);
@override
_SubscriptionListPageState createState() {
return _SubscriptionListPageState();
}
}
class _SubscriptionListPageState extends State<SubscriptionListPage> {
List<String>? sids;
String? title;
bool transitioning = false;
bool unreadOnly = Store.sp.getBool(StoreKeys.UNREAD_SUBS_ONLY) ?? false;
String _search = "";
SubscriptionSortType _sortType = SubscriptionSortType.values[
Store.sp.getInt(StoreKeys.SUBSCRIPTION_SORT) ?? 0];
void _onScrollTop() {
if (widget.scrollTopNotifier.index == 1 &&
!Navigator.of(context).canPop()) {
PrimaryScrollController.of(context).animateTo(
0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
}
}
@override
void initState() {
super.initState();
widget.scrollTopNotifier.addListener(_onScrollTop);
}
@override
void dispose() {
widget.scrollTopNotifier.removeListener(_onScrollTop);
super.dispose();
}
void _openGroups() async {
List<String>? result;
if (Global.isTablet) {
result = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => GroupListPage(),
));
} else {
setState(() {
transitioning = true;
});
result = await CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (context) => GroupListPage(),
);
}
if (!mounted) return;
if (result != null) {
_onScrollTop();
if (result.length == 0) {
setState(() {
title = null;
sids = null;
});
} else if (result.length > 1) {
setState(() {
title = S.of(context).uncategorized;
sids = Global.groupsModel.uncategorized;
});
} else {
setState(() {
title = result![0];
sids = Global.groupsModel.groups[title];
});
}
}
await Future.delayed(Duration(milliseconds: 300));
setState(() {
transitioning = false;
});
}
void _openMarkAllModal() {
showCupertinoModalPopup(
context: context,
builder: (context) =>
MarkAllActionSheet(sids == null ? {} : Set.from(sids!)),
);
}
void _openSettings() {
Navigator.of(context, rootNavigator: true).pushNamed("/settings");
}
void _openErrorLog() {
if (!Global.syncModel.lastSyncSuccess) {
HapticFeedback.mediumImpact();
Navigator.of(context, rootNavigator: true).pushNamed("/error-log");
}
}
void _toggleUnreadOnly() {
HapticFeedback.mediumImpact();
setState(() {
unreadOnly = !unreadOnly;
});
_onScrollTop();
Store.sp.setBool(StoreKeys.UNREAD_SUBS_ONLY, unreadOnly);
}
void _dismissTip() {
if (Global.sourcesModel.showUnreadTip) {
Global.sourcesModel.showUnreadTip = false;
setState(() {});
}
}
static const _iconPadding = Padding(padding: EdgeInsets.only(left: 24));
void _setSortType(SubscriptionSortType type) {
setState(() {
_sortType = type;
});
_onScrollTop();
Store.sp.setInt(StoreKeys.SUBSCRIPTION_SORT, type.index);
}
void _editSearchKeyword() async {
String? keyword = await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => TextEditorPage(
S.of(context).editKeyword,
(v) => v.trim().length > 0,
saveText: S.of(context).search,
initialValue: _search,
navigationBarColor: CupertinoColors.systemBackground,
autocorrect: true,
),
));
if (keyword == null) return;
setState(() {
_search = keyword;
});
_onScrollTop();
}
void _openFilterModal() {
showCupertinoModalPopup(
context: context,
builder: (context) {
final sheet = CupertinoActionSheet(
title: Text(S.of(context).sortBy),
actions: [
CupertinoActionSheetAction(
child: Row(children: [
Icon(CupertinoIcons.time),
Text(S.of(context).sortByLatest),
_iconPadding,
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
_setSortType(SubscriptionSortType.ByLatest);
},
),
CupertinoActionSheetAction(
child: Row(children: [
Icon(CupertinoIcons.sort_up),
Text(S.of(context).sortByNameAsc),
_iconPadding,
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
_setSortType(SubscriptionSortType.ByNameAsc);
},
),
CupertinoActionSheetAction(
child: Row(children: [
Icon(CupertinoIcons.sort_down),
Text(S.of(context).sortByNameDesc),
_iconPadding,
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
_setSortType(SubscriptionSortType.ByNameDesc);
},
),
CupertinoActionSheetAction(
child: Row(children: [
Icon(Icons.radio_button_checked),
Text(S.of(context).sortByUnread),
_iconPadding,
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
_setSortType(SubscriptionSortType.ByUnread);
},
),
CupertinoActionSheetAction(
isDestructiveAction: true,
child: Row(children: [
Icon(CupertinoIcons.search,
color: CupertinoColors.destructiveRed),
Text(_search.length > 0
? S.of(context).editKeyword
: S.of(context).search),
_iconPadding,
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
_editSearchKeyword();
},
),
if (_search.length > 0)
CupertinoActionSheetAction(
isDestructiveAction: true,
child: Row(children: [
Icon(CupertinoIcons.clear_fill,
color: CupertinoColors.destructiveRed),
Text(S.of(context).clearSearch),
_iconPadding,
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
setState(() {
_search = "";
});
_onScrollTop();
},
),
],
cancelButton: CupertinoActionSheetAction(
child: Text(S.of(context).cancel),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
),
);
return ResponsiveActionSheet(sheet);
});
}
Widget _buildUnreadTip() {
return SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.all(16),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Container(
padding: EdgeInsets.all(12),
color: CupertinoColors.secondarySystemBackground.resolveFrom(context),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(right: 12),
child: Icon(Icons.radio_button_checked),
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.of(context).unreadSourceTip,
style: TextStyle(
color: CupertinoColors.label.resolveFrom(context),
fontWeight: FontWeight.bold,
),
),
Padding(padding: EdgeInsets.only(bottom: 6)),
CupertinoButton(
minSize: 28,
padding: EdgeInsets.zero,
child: Text(S.of(context).confirm),
onPressed: _dismissTip,
),
],
)),
],
),
),
),
));
}
@override
Widget build(BuildContext context) {
final titleWidget = GestureDetector(
onLongPress: _toggleUnreadOnly,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
constraints: BoxConstraints(
maxWidth: Global.isTablet
? 260
: MediaQuery.of(context).size.width - 60,
),
child: Text(
title ?? S.of(context).subscriptions,
overflow: TextOverflow.ellipsis,
),
),
if (unreadOnly)
Padding(
padding: EdgeInsets.only(left: 4),
child: Icon(Icons.radio_button_checked, size: 18),
),
if (_search.length > 0)
Padding(
padding: EdgeInsets.only(left: 4),
child: Icon(CupertinoIcons.search, size: 18),
),
],
),
);
final navigationBar = CupertinoSliverNavigationBar(
stretch: false,
largeTitle: titleWidget,
heroTag: "subscriptions",
transitionBetweenRoutes: true,
backgroundColor: transitioning
? MyColors.tileBackground
: CupertinoColors.systemBackground,
leading: CupertinoButton(
minSize: 36,
padding: EdgeInsets.zero,
child: Text(S.of(context).groups),
onPressed: _openGroups,
),
trailing: Container(
transform: Matrix4.translationValues(12, 0, 0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoButton(
padding: EdgeInsets.zero,
child: Icon(
(_sortType != SubscriptionSortType.ByLatest ||
_search.length > 0)
? CupertinoIcons
.line_horizontal_3_decrease_circle_fill
: CupertinoIcons.line_horizontal_3_decrease_circle,
semanticLabel: S.of(context).sortBy,
),
onPressed: _openFilterModal,
),
CupertinoButton(
padding: EdgeInsets.zero,
child: Icon(
CupertinoIcons.checkmark_circle,
semanticLabel: S.of(context).markAll,
),
onPressed: _openMarkAllModal,
),
CupertinoButton(
padding: EdgeInsets.zero,
child: Icon(
CupertinoIcons.settings,
semanticLabel: S.of(context).settings,
),
onPressed: _openSettings,
),
],
),
));
final sourcesList = Consumer<SourcesModel>(
builder: (context, sourcesModel, child) {
List<RSSSource> sources;
if (sids == null) {
sources = Global.sourcesModel.getSources().toList();
if (unreadOnly) {
sources = sources.where((s) => s.unreadCount > 0).toList();
}
} else {
sources = [];
for (var sid in sids!) {
final source = Global.sourcesModel.getSource(sid);
if (source != null && (!unreadOnly || source.unreadCount > 0)) {
sources.add(source);
}
}
}
if (_search.length > 0) {
final keyword = _search.toUpperCase();
sources =
sources.where((s) => s.name.toUpperCase().contains(keyword)).toList();
}
switch (_sortType) {
case SubscriptionSortType.ByLatest:
sources.sort((a, b) => b.latest.compareTo(a.latest));
break;
case SubscriptionSortType.ByNameAsc:
sources.sort((a, b) => a.name.compareTo(b.name));
break;
case SubscriptionSortType.ByNameDesc:
sources.sort((a, b) => b.name.compareTo(a.name));
break;
case SubscriptionSortType.ByUnread:
sources.sort((a, b) => b.unreadCount.compareTo(a.unreadCount));
break;
}
return SliverList(
delegate: SliverChildBuilderDelegate((content, index) {
var source = sources[index];
return SubscriptionItem(source, key: Key(source.id));
}, childCount: sources.length),
);
},
);
final syncStyle = TextStyle(
fontSize: 14,
color: CupertinoColors.tertiaryLabel.resolveFrom(context),
);
final syncInfo = Consumer<SyncModel>(
builder: (context, syncModel, child) {
return SliverToBoxAdapter(
child: GestureDetector(
onLongPress: _openErrorLog,
child: Container(
padding: EdgeInsets.all(12),
child: Column(
children: [
Text(
syncModel.lastSyncSuccess
? S.of(context).lastSyncSuccess
: S.of(context).lastSyncFailure,
style: syncStyle,
),
Text(
DateFormat.Md(Localizations.localeOf(context).toString())
.add_Hm()
.format(syncModel.lastSynced),
style: syncStyle,
),
],
),
),
),
);
},
);
return CupertinoScrollbar(
child: CustomScrollView(slivers: [
navigationBar,
SyncControl(),
if (Global.sourcesModel.showUnreadTip) _buildUnreadTip(),
if (sids != null && sids!.length > 0)
Consumer<SourcesModel>(
builder: (context, sourcesModel, child) {
var count = sids!
.map((sid) => sourcesModel.getSource(sid))
.fold(0, (c, s) => c + (s?.unreadCount ?? 0));
return SliverToBoxAdapter(
child: MyListTile(
title: Text(S.of(context).allArticles),
trailing: count > 0 ? Badge(count) : null,
trailingChevron: false,
onTap: () async {
await Global.feedsModel.initSourcesFeed(sids!.toList());
Navigator.of(context).pushNamed("/feed", arguments: title);
},
background: CupertinoColors.systemBackground,
));
},
),
sourcesList,
syncInfo,
]));
}
}