M assets/article/article.css => assets/article/article.css +4 -0
@@ 126,4 126,8 @@ article ul, article menu, article dir {
}
article li {
overflow: visible;
+}
+article pre {
+ white-space: pre-wrap;
+ word-break: break-all;
}=
\ No newline at end of file
M lib/l10n/intl_en.arb => lib/l10n/intl_en.arb +3 -1
@@ 88,5 88,7 @@
"retry": "Retry",
"copy": "Copy",
"errorLog": "Error log",
- "unreadSourceTip": "You can long press on the title of this page to toggle between all and unread subscriptions."
+ "unreadSourceTip": "You can long press on the title of this page to toggle between all and unread subscriptions.",
+ "uncategorized": "Uncategorized",
+ "showUncategorized": "Show uncategorized"
}=
\ No newline at end of file
M lib/l10n/intl_zh.arb => lib/l10n/intl_zh.arb +3 -1
@@ 88,5 88,7 @@
"retry": "重试",
"copy": "复制",
"errorLog": "错误日志",
- "unreadSourceTip": "您可以长按此页面的标题来切换全部订阅源或仅未读订阅源。"
+ "unreadSourceTip": "您可以长按此页面的标题来切换全部订阅源或仅未读订阅源。",
+ "uncategorized": "未分组",
+ "showUncategorized": "显示“未分组”"
}=
\ No newline at end of file
M lib/models/groups_model.dart => lib/models/groups_model.dart +30 -0
@@ 1,8 1,10 @@
+import 'package:fluent_reader_lite/utils/global.dart';
import 'package:fluent_reader_lite/utils/store.dart';
import 'package:flutter/cupertino.dart';
class GroupsModel with ChangeNotifier {
Map<String, List<String>> _groups = Store.getGroups();
+ List<String> uncategorized = Store.getUncategorized();
Map<String, List<String>> get groups => _groups;
set groups(Map<String, List<String>> groups) {
@@ 10,4 12,32 @@ class GroupsModel with ChangeNotifier {
notifyListeners();
Store.setGroups(groups);
}
+
+ void updateUncategorized({force: false}) {
+ if (uncategorized != null || force) {
+ final sids = Set<String>.from(
+ Global.sourcesModel.getSources().map<String>((s) => s.id)
+ );
+ for (var group in _groups.values) {
+ for (var sid in group) {
+ sids.remove(sid);
+ }
+ }
+ uncategorized = sids.toList();
+ Store.setUncategorized(uncategorized);
+ }
+ }
+
+ bool get showUncategorized => uncategorized != null;
+ set showUncategorized(bool value) {
+ if (showUncategorized != value) {
+ if (value) {
+ updateUncategorized(force: true);
+ } else {
+ uncategorized = null;
+ Store.setUncategorized(null);
+ }
+ notifyListeners();
+ }
+ }
}=
\ No newline at end of file
M lib/pages/group_list_page.dart => lib/pages/group_list_page.dart +23 -5
@@ 18,6 18,8 @@ class GroupListPage extends StatefulWidget {
}
class _GroupListPageState extends State<GroupListPage> {
+ static const List<String> _uncategorizedIndicator = [null, null];
+
int _unreadCount(Iterable<RSSSource> sources) {
return sources.fold(0, (c, s) => c + (s != null ? s.unreadCount : 0));
}
@@ 55,27 57,43 @@ class _GroupListPageState extends State<GroupListPage> {
builder: (context, groupsModel, sourcesModel, child) {
final groupNames = groupsModel.groups.keys.toList();
groupNames.sort(Utils.localStringCompare);
+ if (groupsModel.uncategorized != null) {
+ groupNames.insert(0, null);
+ }
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
- final groupName = groupNames[index];
+ String groupName;
+ List<String> group;
+ final isUncategorized = groupsModel.showUncategorized && index == 0;
+ if (isUncategorized) {
+ groupName = S.of(context).uncategorized;
+ group = groupsModel.uncategorized;
+ } else {
+ groupName = groupNames[index];
+ group = groupsModel.groups[groupName];
+ }
final count = _unreadCount(
- groupsModel.groups[groupName].map((sid) => sourcesModel.getSource(sid))
+ group.map((sid) => sourcesModel.getSource(sid))
);
final tile = MyListTile(
title: Flexible(child: Text(groupName, overflow: TextOverflow.ellipsis)),
trailing: count > 0 ? Badge(count) : null,
- onTap: () { Navigator.of(context).pop([groupName]); },
+ onTap: () {
+ Navigator.of(context).pop(
+ isUncategorized ? _uncategorizedIndicator : [groupName]
+ );
+ },
background: CupertinoColors.systemBackground,
);
return Dismissible(
- key: Key(groupName),
+ key: Key("$groupName$index"),
child: tile,
background: dismissBg,
direction: DismissDirection.startToEnd,
dismissThresholds: _dismissThresholds,
confirmDismiss: (_) async {
HapticFeedback.mediumImpact();
- Set<String> sids = Set.from(groupsModel.groups[groupName]);
+ Set<String> sids = Set.from(group);
showCupertinoModalPopup(
context: context,
builder: (context) => MarkAllActionSheet(sids),
M lib/pages/settings/feed_page.dart => lib/pages/settings/feed_page.dart +43 -25
@@ 2,6 2,7 @@ import 'package:fluent_reader_lite/components/list_tile_group.dart';
import 'package:fluent_reader_lite/components/my_list_tile.dart';
import 'package:fluent_reader_lite/generated/l10n.dart';
import 'package:fluent_reader_lite/models/feeds_model.dart';
+import 'package:fluent_reader_lite/models/groups_model.dart';
import 'package:fluent_reader_lite/utils/colors.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
@@ 56,35 57,52 @@ class FeedPage extends StatelessWidget {
ItemSwipeOption.OpenExternal: S.of(context).openExternal,
ItemSwipeOption.OpenMenu: S.of(context).openMenu,
};
- return ListView(
- children: [
- ListTileGroup([
- MyListTile(
- title: Text(S.of(context).showThumb),
- trailing: CupertinoSwitch(
- value: feedsModel.showThumb,
- onChanged: (v) { feedsModel.showThumb = v; },
- ),
- trailingChevron: false,
- ),
- MyListTile(
- title: Text(S.of(context).showSnippet),
- trailing: CupertinoSwitch(
- value: feedsModel.showSnippet,
- onChanged: (v) { feedsModel.showSnippet = v; },
- ),
- trailingChevron: false,
- ),
- MyListTile(
- title: Text(S.of(context).dimRead),
+ final preferences = ListTileGroup([
+ MyListTile(
+ title: Text(S.of(context).showThumb),
+ trailing: CupertinoSwitch(
+ value: feedsModel.showThumb,
+ onChanged: (v) { feedsModel.showThumb = v; },
+ ),
+ trailingChevron: false,
+ ),
+ MyListTile(
+ title: Text(S.of(context).showSnippet),
+ trailing: CupertinoSwitch(
+ value: feedsModel.showSnippet,
+ onChanged: (v) { feedsModel.showSnippet = v; },
+ ),
+ trailingChevron: false,
+ ),
+ MyListTile(
+ title: Text(S.of(context).dimRead),
+ trailing: CupertinoSwitch(
+ value: feedsModel.dimRead,
+ onChanged: (v) { feedsModel.dimRead = v; },
+ ),
+ trailingChevron: false,
+ withDivider: false,
+ ),
+ ], title: S.of(context).preferences);
+ final groups = ListTileGroup([
+ Consumer<GroupsModel>(
+ builder: (context, groupsModel, child) {
+ return MyListTile(
+ title: Text(S.of(context).showUncategorized),
trailing: CupertinoSwitch(
- value: feedsModel.dimRead,
- onChanged: (v) { feedsModel.dimRead = v; },
+ value: groupsModel.showUncategorized,
+ onChanged: (v) { groupsModel.showUncategorized = v; },
),
trailingChevron: false,
withDivider: false,
- ),
- ], title: S.of(context).preferences),
+ );
+ },
+ ),
+ ], title: S.of(context).groups);
+ return ListView(
+ children: [
+ preferences,
+ groups,
ListTileGroup([
MyListTile(
title: Text(S.of(context).swipeRight),
M lib/pages/subscription_list_page.dart => lib/pages/subscription_list_page.dart +6 -0
@@ 73,11 73,17 @@ class _SubscriptionListPageState extends State<SubscriptionListPage> {
}
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];
M lib/utils/store.dart => lib/utils/store.dart +16 -0
@@ 7,6 7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
abstract class StoreKeys {
static const GROUPS = "groups";
static const ERROR_LOG = "errorLog";
+ static const UNCATEGORIZED = "uncategorized";
// General
static const THEME = "theme";
@@ 92,6 93,21 @@ class Store {
sp.setString(StoreKeys.GROUPS, jsonEncode(groups));
}
+ static List<String> getUncategorized() {
+ final stored = sp.getString(StoreKeys.UNCATEGORIZED);
+ if (stored == null) return null;
+ final parsed = jsonDecode(stored);
+ return List.castFrom(parsed);
+ }
+
+ static void setUncategorized(List<String> value) {
+ if (value == null) {
+ sp.remove(StoreKeys.UNCATEGORIZED);
+ } else {
+ sp.setString(StoreKeys.UNCATEGORIZED, jsonEncode(value));
+ }
+ }
+
static int getArticleFontSize() {
return sp.getInt(StoreKeys.ARTICLE_FONT_SIZE) ?? 16;
}