From 918387ace1397b6bd4b3c32cd0783977562bfcdb Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Mon, 29 Apr 2024 02:19:29 +0800 Subject: [PATCH] wip: add rss support --- lib/const.dart | 12 ++++ lib/main.dart | 2 + lib/model/config.dart | 7 +- lib/page/feed.dart | 144 ++++++++++++++++++++++++++++++++++++++++++ lib/page/home.dart | 4 +- pubspec.lock | 23 +++++++ pubspec.yaml | 2 + 7 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 lib/page/feed.dart diff --git a/lib/const.dart b/lib/const.dart index 5f46f07..9f020da 100644 --- a/lib/const.dart +++ b/lib/const.dart @@ -3,3 +3,15 @@ const String defaultThemeName = 'Arc-Dark'; const String searchModeVideoShortcut = 'v '; const String searchModeChannelShortcut = 'c '; const String searchModePlaylistShortcut = 'p '; + +const List configPathLookup = [ + '/yatta/config.yaml', + '/yatta/config.yml', + '/yatta/config.json', +]; + +const List feedPathLookup = [ + '/yatta/feed.yaml', + '/yatta/feed.yml', + '/yatta/feed.json', +]; diff --git a/lib/main.dart b/lib/main.dart index 0e70547..73ce3cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'model/config.dart'; import 'model/setting_options.dart'; import 'model/state.dart'; import 'model/theme.dart'; +import 'page/feed.dart'; import 'page/history.dart'; import 'page/home.dart'; import 'page/playlist.dart'; @@ -147,6 +148,7 @@ class App extends StatelessWidget { routes: { '/': (final _) => const HomePage(), '/playlist': (final _) => const PlaylistPage(), + '/feed': (final _) => const FeedPage(), '/history': (final _) => const HistoryPage(), '/settings': (final _) => const SettingsPage(), }, diff --git a/lib/model/config.dart b/lib/model/config.dart index 24a97e9..1f63c4e 100644 --- a/lib/model/config.dart +++ b/lib/model/config.dart @@ -4,14 +4,9 @@ import 'package:xdg_directories/xdg_directories.dart'; import 'package:yaml/yaml.dart'; import 'package:yaml_edit/yaml_edit.dart'; +import '../const.dart'; import 'setting_options.dart'; -const List configPathLookup = [ - '/yatta/config.yaml', - '/yatta/config.yml', - '/yatta/config.json', -]; - abstract class YamlConfig { YamlConfig({ final String? filePath, diff --git a/lib/page/feed.dart b/lib/page/feed.dart new file mode 100644 index 0000000..50a40a0 --- /dev/null +++ b/lib/page/feed.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:autoscroll/autoscroll.dart'; +import 'package:collection/collection.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:http/http.dart' as http; +import 'package:rss_dart/dart_rss.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:xdg_directories/xdg_directories.dart' as xdg; +import 'package:yaml/yaml.dart' as yaml; +import 'package:youtube_api/youtube_api.dart'; + +import '../../intent.dart'; +import '../const.dart'; +import '../helper/command_parser.dart'; +import '../widget/keyboard_navigation.dart'; +import '../widget/list_items/list_item.dart'; + +class FeedPage extends StatefulWidget { + const FeedPage({super.key}); + + @override + State createState() => _FeedPageState(); +} + +class _FeedPageState extends State { + final FocusNode searchBarFocus = FocusNode(); + List? filteredList; + List? feedList; + late final Map> _actionMap = { + SearchBarFocusIntent: CallbackAction( + onInvoke: (final _) => _requestSearchBarFocus(), + ), + NavigationPopIntent: CallbackAction( + onInvoke: (final _) => _navigationPop(context), + ) + }; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((final _) async { + await fetchFeed(); + }); + } + + Future fetchFeed() async { + final file = feedPathLookup + .map((final path) => File('${xdg.configHome.path}$path')) + .firstWhereOrNull((final configLookup) => configLookup.existsSync()); + + if (file == null) return; + + final result = await file + .readAsString() + .then((final rawFile) => switch (yaml.loadYaml(rawFile)) { + final List urls => urls + .map((final url) => http.get(Uri.parse(url.toString()))) + .toList(), + _ => >[] + }) + .then((final urls) async => Future.wait(urls)); + + setState(() { + feedList = result + .map((final e) => AtomFeed.parse(e.body)) + .expand((final e) => e.items) + .sorted((final a, final b) => DateTime.parse(a.published ?? '') + .compareTo(DateTime.parse(b.published ?? ''))) + .toList(); + + filteredList = feedList; + }); + } + + void _requestSearchBarFocus() => searchBarFocus.requestFocus(); + + void _navigationPop(final BuildContext context) { + if (!Navigator.of(context).canPop()) return; + Navigator.of(context).pop(); + } + + void _filterList(final String keyword) { + if (filteredList == null) return; + setState(() { + filteredList = feedList + ?.where((final e) => + e.toString().toLowerCase().contains(keyword.toLowerCase())) + .toList(); + }); + } + + @override + Widget build(final BuildContext context) { + return Actions( + actions: _actionMap, + child: NavigationView( + appBar: NavigationAppBar( + title: SizedBox( + height: 36, + child: TextBox( + focusNode: searchBarFocus, + placeholder: 'Search from feed', + onChanged: _filterList, + ), + ), + ), + content: KeyboardNavigation( + child: AutoscrollListView.builder( + itemCount: filteredList?.length ?? 0, + itemBuilder: (final context, final index) { + final youtubeVideo = + filteredList![filteredList!.length - index - 1]; + // print(youtubeVideo + // .items.first.media?.group?.); + + final listItem = ListItemVideo( + title: youtubeVideo.media?.title?.value ?? '', + channelTitle: youtubeVideo.authors.first.name ?? '', + description: + youtubeVideo.media?.group?.description?.value ?? '', + duration: '00:02', + thumbnailUrl: + youtubeVideo.media?.group?.thumbnail.firstOrNull?.url ?? '', + publishedAt: youtubeVideo.published ?? '', + ); + + return listItem; + + return ListItem( + autofocus: index == 0, + youtubeVideo: YoutubeVideo(''), + fromHistory: true, + child: listItem, + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/page/home.dart b/lib/page/home.dart index 0b3ab45..5a8c5bf 100644 --- a/lib/page/home.dart +++ b/lib/page/home.dart @@ -46,7 +46,9 @@ class WelcomeMessage extends StatelessWidget { title: 'Feed', subtitle: 'Subsribe to channels via RSS Feed', icon: FluentIcons.content_feed, - onPressed: () {}, + onPressed: () async { + await Navigator.of(context).pushNamed('/feed'); + }, ), _SelectionMenu( key: const Key('history'), diff --git a/pubspec.lock b/pubspec.lock index 208efe2..74502d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -444,6 +444,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -516,6 +524,13 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.10" + rss_dart: + dependency: "direct main" + description: + path: "../rss.dart" + relative: true + source: path + version: "1.0.6" rxdart: dependency: transitive description: @@ -873,6 +888,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5329ed3..79edf2b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,8 @@ dependencies: flutter: sdk: flutter flutter_riverpod: ^2.3.6 + rss_dart: + path: ../rss.dart/ shared_preferences: ^2.1.2 url_launcher: ^6.1.11 yaml_edit: ^2.2.0