diff --git a/lib/extractor/general/world/apnews.dart b/lib/extractor/general/world/apnews.dart index 25888f6..313a5b9 100644 --- a/lib/extractor/general/world/apnews.dart +++ b/lib/extractor/general/world/apnews.dart @@ -26,7 +26,6 @@ class APNews extends Publisher { var document = html_parser.parse(utf8.decode(response.bodyBytes)); document .querySelectorAll('.Page-header-navigation .AnClick-MainNav') - .take(5) .forEach((element) { map.putIfAbsent( element.text, diff --git a/lib/pages/category_selector.dart b/lib/pages/category_selector.dart new file mode 100644 index 0000000..6179130 --- /dev/null +++ b/lib/pages/category_selector.dart @@ -0,0 +1,276 @@ +import 'package:flutter/material.dart'; +import 'package:raven/model/publisher.dart'; +import 'package:raven/model/user_subscription.dart'; +import 'package:raven/utils/store.dart'; + +class CategorySelector extends StatefulWidget { + final Map publishers; + final String newsSource; + final VoidCallback callback; + + const CategorySelector(this.publishers, this.newsSource, + {super.key, required this.callback}); + + @override + State createState() => _CategorySelectorState(); +} + +class _CategorySelectorState extends State { + List selectedSubscriptions = []; // List + List customSubscriptions = []; // List + String customCategory = ""; + TextEditingController customCategoryController = TextEditingController(); + Future>? future; + + @override + void initState() { + setState(() { + selectedSubscriptions = Store.selectedSubscriptions; + customSubscriptions = Store.customSubscriptions + .where((element) => element.publisher == widget.newsSource) + .toList(); + }); + super.initState(); + future = widget.publishers[widget.newsSource]?.categories; + } + + String convertString(String input) { + List parts = input.split('/'); + parts.removeWhere((part) => part.isEmpty); + List capitalizedParts = parts.map((part) { + return capitalize(part); + }).toList(); + String result = capitalizedParts.join('/'); + return result; + } + + String capitalize(String string) { + return "${string[0].toUpperCase()}${string.substring(1)}"; + } + + @override + Widget build(BuildContext context) { + int customSubsSize = customSubscriptions + .where((element) => element.publisher == widget.newsSource) + .length; + return Scaffold( + appBar: AppBar( + title: Text("${widget.newsSource} categories"), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: FutureBuilder( + future: future, + builder: (context, snapshot) { + if (snapshot.hasData) { + return ListView.builder( + shrinkWrap: true, + itemCount: snapshot.data!.length + 3 + customSubsSize, + itemBuilder: (context, index) { + var subCategoryKey = "All"; + var subCategoryValue = "/"; + var userSubscription = + UserSubscription(widget.newsSource, subCategoryValue); + if (index == 0) { + return _buildAllCheckbox(subCategoryKey, userSubscription); + } + if (index - 1 < snapshot.data!.length) { + subCategoryKey = snapshot.data!.keys.toList()[index - 1]; + subCategoryValue = snapshot.data!.values.toList()[index - 1]; + userSubscription = + UserSubscription(widget.newsSource, subCategoryValue); + return _buildCategorySelector( + subCategoryKey, userSubscription); + } else if (index > snapshot.data!.length && + index < customSubsSize + snapshot.data!.length + 1) { + return customCategorySaved(index, snapshot); + } else if (index == + (snapshot.data!.length + 1 + customSubsSize)) { + return Flex( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + direction: Axis.horizontal, + children: [ + _buildCustomSourceSelector(), + if (customCategory.isEmpty) + const Flexible(child: SizedBox.shrink()) + else + _buildCustomTester() + ], + ); + } else { + return SaveButton( + selectedSubscriptions: selectedSubscriptions, + widget: widget, + ); + } + }, + ); + } else if (snapshot.hasError) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text(snapshot.error.toString()), + ); + } + return const Center( + child: CircularProgressIndicator(), + ); + }, + ), + ), + ); + } + + CheckboxListTile _buildAllCheckbox( + String subCategoryKey, UserSubscription userSubscription) { + return CheckboxListTile( + title: Text(subCategoryKey), + value: selectedSubscriptions.contains(userSubscription), + onChanged: (value) { + if (value!) { + selectedSubscriptions.removeWhere((element) { + return element.publisher == userSubscription.publisher; + }); + } + updateList(value, userSubscription); + }, + ); + } + + CheckboxListTile _buildCategorySelector( + String subCategoryKey, UserSubscription userSubscription) { + return CheckboxListTile( + title: Text(subCategoryKey), + value: selectedSubscriptions.contains(userSubscription), + onChanged: selectedSubscriptions + .where((element) => + element.publisher == userSubscription.publisher && + element.category == "/") + .isNotEmpty + ? null + : (value) { + updateList(value, userSubscription); + }, + ); + } + + CheckboxListTile customCategorySaved( + int index, AsyncSnapshot> snapshot) { + return CheckboxListTile( + secondary: IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: () { + var subscription = + customSubscriptions[index - (snapshot.data!.length + 1)]; + setState(() { + customSubscriptions.remove(subscription); + selectedSubscriptions.remove(subscription); + }); + var cs = Store.customSubscriptions; + cs.remove(subscription); + Store.customSubscriptions = cs; + var ss = Store.selectedSubscriptions; + ss.remove(subscription); + Store.selectedSubscriptions = ss; + }), + title: Text(convertString( + (customSubscriptions[index - (snapshot.data!.length + 1)] + as UserSubscription) + .category)), + value: selectedSubscriptions + .contains(customSubscriptions[index - (snapshot.data!.length + 1)]), + onChanged: (value) { + updateList( + value, customSubscriptions[index - (snapshot.data!.length + 1)]); + }, + ); + } + + Widget _buildCustomSourceSelector() { + return Flexible( + flex: 3, + fit: FlexFit.tight, + child: TextField( + controller: customCategoryController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: "Custom category", + ), + onEditingComplete: () { + setState(() { + customCategory = customCategoryController.text; + }); + }, + )); + } + + Flexible _buildCustomTester() { + return Flexible( + flex: 1, + child: FutureBuilder( + future: widget.publishers[widget.newsSource] + ?.articles(category: customCategory), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!.isNotEmpty + ? IconButton( + onPressed: () { + setState(() { + customSubscriptions.add(UserSubscription( + widget.newsSource, + customCategory, + )); + }); + Store.customSubscriptions += [ + UserSubscription( + widget.newsSource, + customCategory, + ) + ]; + }, + icon: const Icon(Icons.save_alt)) + : const Icon(Icons.cancel); + } else if (snapshot.hasError) { + const Icon(Icons.cancel); + } + return const CircularProgressIndicator(); + }, + ), + ); + } + + void updateList(bool? value, UserSubscription userSubscription) { + setState(() { + if (value!) { + selectedSubscriptions.add(userSubscription); + } else { + selectedSubscriptions.remove(userSubscription); + } + }); + } +} + +class SaveButton extends StatelessWidget { + const SaveButton({ + super.key, + required this.selectedSubscriptions, + required this.widget, + }); + + final List selectedSubscriptions; + final CategorySelector widget; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: FilledButton( + onPressed: () { + Store.selectedSubscriptions = selectedSubscriptions; + Navigator.of(context).pop(); + widget.callback(); + }, + child: const Text("SAVE"), + ), + ); + } +} diff --git a/lib/pages/subscription.dart b/lib/pages/subscription.dart index 38ea347..6bb91db 100644 --- a/lib/pages/subscription.dart +++ b/lib/pages/subscription.dart @@ -3,9 +3,10 @@ import 'dart:core'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:raven/model/publisher.dart'; -import 'package:raven/model/user_subscription.dart'; import 'package:raven/utils/store.dart'; +import 'category_selector.dart'; + class SubscriptionsPage extends StatefulWidget { const SubscriptionsPage({super.key}); @@ -20,6 +21,7 @@ class _SubscriptionsPageState extends State TextEditingController searchController = TextEditingController(); bool _isSearching = false; int? _value = 0; + @override void initState() { filteredNewsSources = newsSources; @@ -116,16 +118,20 @@ class _SubscriptionsPageState extends State ? SizedBox.shrink() : Icon(Icons.check_circle), onTap: () { - showDialog( - context: context, - builder: (context) { - return CategoryPopup(publishers, newsSource, - callback: () { - setState(() { - categories = getSelectedCategories(newsSource); - }); - }); - }, + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => CategorySelector( + publishers, + newsSource, + callback: () { + setState( + () { + categories = getSelectedCategories(newsSource); + }, + ); + }, + ), + ), ); }, ); @@ -158,245 +164,3 @@ class _SubscriptionsPageState extends State @override bool get wantKeepAlive => true; } - -class CategoryPopup extends StatefulWidget { - final Map publishers; - final String newsSource; - final VoidCallback callback; - - const CategoryPopup(this.publishers, this.newsSource, - {super.key, required this.callback}); - - @override - State createState() => _CategoryPopupState(); -} - -class _CategoryPopupState extends State { - List selectedSubscriptions = []; // List - List customSubscriptions = []; // List - String customCategory = ""; - TextEditingController customCategoryController = TextEditingController(); - Future>? future; - - @override - void initState() { - setState(() { - selectedSubscriptions = Store.selectedSubscriptions; - customSubscriptions = Store.customSubscriptions - .where((element) => element.publisher == widget.newsSource) - .toList(); - }); - super.initState(); - future = widget.publishers[widget.newsSource]?.categories; - } - - String convertString(String input) { - List parts = input.split('/'); - parts.removeWhere((part) => part.isEmpty); - List capitalizedParts = parts.map((part) { - return capitalize(part); - }).toList(); - String result = capitalizedParts.join('/'); - return result; - } - - String capitalize(String string) { - return "${string[0].toUpperCase()}${string.substring(1)}"; - } - - @override - Widget build(BuildContext context) { - return Dialog( - child: FutureBuilder( - future: future, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: ListView( - shrinkWrap: true, - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - "Categories", - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.w500), - ), - ), - ListView.builder( - shrinkWrap: true, - itemCount: snapshot.data!.length + 1, - itemBuilder: (context, categoryIndex) { - var subCategoryKey = "All"; - var subCategoryValue = "/"; - var userSubscription = - UserSubscription(widget.newsSource, subCategoryValue); - if (categoryIndex == 0) { - // All checkbox - return CheckboxListTile( - title: Text(subCategoryKey), - value: - selectedSubscriptions.contains(userSubscription), - onChanged: (value) { - if (value!) { - selectedSubscriptions.removeWhere((element) { - return element.publisher == - userSubscription.publisher; - }); - } - updateList(value, userSubscription); - }, - ); - } - subCategoryKey = - snapshot.data!.keys.toList()[categoryIndex - 1]; - subCategoryValue = - snapshot.data!.values.toList()[categoryIndex - 1]; - userSubscription = - UserSubscription(widget.newsSource, subCategoryValue); - // category checkbox - return CheckboxListTile( - title: Text(subCategoryKey), - value: selectedSubscriptions.contains(userSubscription), - onChanged: selectedSubscriptions - .where((element) => - element.publisher == - userSubscription.publisher && - element.category == "/") - .isNotEmpty - ? null - : (value) { - updateList(value, userSubscription); - }, - ); - }, - ), - ListView.builder( - shrinkWrap: true, - itemCount: customSubscriptions - .where( - (element) => element.publisher == widget.newsSource) - .length, - itemBuilder: (context, index) { - return CheckboxListTile( - secondary: IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: () { - var subscription = customSubscriptions[index]; - setState(() { - customSubscriptions.remove(subscription); - selectedSubscriptions.remove(subscription); - }); - var cs = Store.customSubscriptions; - cs.remove(subscription); - Store.customSubscriptions = cs; - var ss = Store.selectedSubscriptions; - ss.remove(subscription); - Store.selectedSubscriptions = ss; - }), - title: Text(convertString( - (customSubscriptions[index] as UserSubscription) - .category)), - value: selectedSubscriptions - .contains(customSubscriptions[index]), - onChanged: (value) { - updateList(value, customSubscriptions[index]); - }, - ); - }, - ), - Flex( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - direction: Axis.horizontal, - children: [ - Flexible( - flex: 3, - child: TextField( - controller: customCategoryController, - decoration: const InputDecoration( - hintText: "Custom category"), - onEditingComplete: () { - setState(() { - customCategory = customCategoryController.text; - }); - }, - )), - if (customCategory.isEmpty) - const Flexible(child: SizedBox.shrink()) - else - Flexible( - flex: 1, - child: FutureBuilder( - future: widget.publishers[widget.newsSource] - ?.articles(category: customCategory), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!.isNotEmpty - ? IconButton( - onPressed: () { - setState(() { - customSubscriptions - .add(UserSubscription( - widget.newsSource, - customCategory, - )); - }); - Store.customSubscriptions += [ - UserSubscription( - widget.newsSource, - customCategory, - ) - ]; - }, - icon: const Icon(Icons.save_alt)) - : const Icon(Icons.cancel); - } else if (snapshot.hasError) { - const Icon(Icons.cancel); - } - return const CircularProgressIndicator(); - }, - ), - ) - ], - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: FilledButton( - onPressed: () { - Store.selectedSubscriptions = selectedSubscriptions; - Navigator.of(context).pop(); - widget.callback(); - }, - child: const Text("SAVE"), - ), - ), - ], - ), - ); - } else if (snapshot.hasError) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text(snapshot.error.toString()), - ); - } - return const Padding( - padding: EdgeInsets.all(8.0), - child: Text("Loading"), - ); - }, - ), - ); - } - - void updateList(bool? value, UserSubscription userSubscription) { - setState(() { - if (value!) { - selectedSubscriptions.add(userSubscription); - } else { - selectedSubscriptions.remove(userSubscription); - } - }); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index d72fd14..e1d959f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: raven description: "News aggregator" publish_to: 'none' -version: 0.8.2+20 +version: 0.8.3+21 environment: sdk: '>=3.2.1 <4.0.0'