diff --git a/packages/neon/lib/src/apps/news/app.dart b/packages/neon/lib/src/apps/news/app.dart index 73bfc393..f5ff6da6 100644 --- a/packages/neon/lib/src/apps/news/app.dart +++ b/packages/neon/lib/src/apps/news/app.dart @@ -10,8 +10,10 @@ import 'package:html/dom.dart' as html_dom; import 'package:html/parser.dart' as html_parser; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:neon/l10n/localizations.dart'; +import 'package:neon/src/apps/news/blocs/article.dart'; import 'package:neon/src/apps/news/blocs/articles.dart'; import 'package:neon/src/apps/news/blocs/news.dart'; +import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/neon.dart'; import 'package:nextcloud/nextcloud.dart'; diff --git a/packages/neon/lib/src/apps/news/blocs/article.dart b/packages/neon/lib/src/apps/news/blocs/article.dart new file mode 100644 index 00000000..c49aef88 --- /dev/null +++ b/packages/neon/lib/src/apps/news/blocs/article.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:neon/src/apps/news/blocs/articles.dart'; +import 'package:neon/src/neon.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:rx_bloc/rx_bloc.dart'; +import 'package:rxdart/rxdart.dart'; + +part 'article.rxb.g.dart'; + +abstract class NewsArticleBlocEvents { + void markArticleAsRead(); + + void markArticleAsUnread(); + + void starArticle(); + + void unstarArticle(); +} + +abstract class NewsArticleBlocStates { + BehaviorSubject get unread; + + BehaviorSubject get starred; + + Stream get errors; +} + +@RxBloc() +class NewsArticleBloc extends $NewsArticleBloc { + NewsArticleBloc( + this._requestManager, + this._client, + this._newsArticlesBloc, + final NewsArticle article, + ) { + _$markArticleAsReadEvent.listen((final _) { + _wrapArticleAction(() async { + await _client.news.markArticleAsRead(itemId: article.id); + _unreadSubject.add(false); + }); + }); + + _$markArticleAsUnreadEvent.listen((final _) { + _wrapArticleAction(() async { + await _client.news.markArticleAsUnread(itemId: article.id); + _unreadSubject.add(true); + }); + }); + + _$starArticleEvent.listen((final _) { + _wrapArticleAction(() async { + await _client.news.starArticle(itemId: article.id); + _starredSubject.add(true); + }); + }); + + _$unstarArticleEvent.listen((final _) { + _wrapArticleAction(() async { + await _client.news.unstarArticle(itemId: article.id); + _starredSubject.add(false); + }); + }); + + _unreadSubject.add(article.unread); + _starredSubject.add(article.starred); + url = article.url; + } + + void _wrapArticleAction(final Future Function() call) { + final stream = _requestManager.wrapWithoutCache(() async => call()).asBroadcastStream(); + stream.whereError().listen(_errorsStreamController.add); + stream.whereSuccess().listen((final _) async { + _newsArticlesBloc.refresh(); + }); + } + + final RequestManager _requestManager; + final NextcloudClient _client; + final NewsArticlesBloc _newsArticlesBloc; + + late final String url; + final _unreadSubject = BehaviorSubject(); + final _starredSubject = BehaviorSubject(); + final _errorsStreamController = StreamController(); + + @override + void dispose() { + unawaited(_unreadSubject.close()); + unawaited(_starredSubject.close()); + unawaited(_errorsStreamController.close()); + super.dispose(); + } + + @override + BehaviorSubject _mapToUnreadState() => _unreadSubject; + + @override + BehaviorSubject _mapToStarredState() => _starredSubject; + + @override + Stream _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream(); +} diff --git a/packages/neon/lib/src/apps/news/blocs/article.rxb.g.dart b/packages/neon/lib/src/apps/news/blocs/article.rxb.g.dart new file mode 100644 index 00000000..7d5aee70 --- /dev/null +++ b/packages/neon/lib/src/apps/news/blocs/article.rxb.g.dart @@ -0,0 +1,85 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Generator: RxBlocGeneratorForAnnotation +// ************************************************************************** + +part of 'article.dart'; + +/// Used as a contractor for the bloc, events and states classes +/// {@nodoc} +abstract class NewsArticleBlocType extends RxBlocTypeBase { + NewsArticleBlocEvents get events; + NewsArticleBlocStates get states; +} + +/// [$NewsArticleBloc] extended by the [NewsArticleBloc] +/// {@nodoc} +abstract class $NewsArticleBloc extends RxBlocBase + implements NewsArticleBlocEvents, NewsArticleBlocStates, NewsArticleBlocType { + final _compositeSubscription = CompositeSubscription(); + + /// Тhe [Subject] where events sink to by calling [markArticleAsRead] + final _$markArticleAsReadEvent = PublishSubject(); + + /// Тhe [Subject] where events sink to by calling [markArticleAsUnread] + final _$markArticleAsUnreadEvent = PublishSubject(); + + /// Тhe [Subject] where events sink to by calling [starArticle] + final _$starArticleEvent = PublishSubject(); + + /// Тhe [Subject] where events sink to by calling [unstarArticle] + final _$unstarArticleEvent = PublishSubject(); + + /// The state of [unread] implemented in [_mapToUnreadState] + late final BehaviorSubject _unreadState = _mapToUnreadState(); + + /// The state of [starred] implemented in [_mapToStarredState] + late final BehaviorSubject _starredState = _mapToStarredState(); + + /// The state of [errors] implemented in [_mapToErrorsState] + late final Stream _errorsState = _mapToErrorsState(); + + @override + void markArticleAsRead() => _$markArticleAsReadEvent.add(null); + + @override + void markArticleAsUnread() => _$markArticleAsUnreadEvent.add(null); + + @override + void starArticle() => _$starArticleEvent.add(null); + + @override + void unstarArticle() => _$unstarArticleEvent.add(null); + + @override + BehaviorSubject get unread => _unreadState; + + @override + BehaviorSubject get starred => _starredState; + + @override + Stream get errors => _errorsState; + + BehaviorSubject _mapToUnreadState(); + + BehaviorSubject _mapToStarredState(); + + Stream _mapToErrorsState(); + + @override + NewsArticleBlocEvents get events => this; + + @override + NewsArticleBlocStates get states => this; + + @override + void dispose() { + _$markArticleAsReadEvent.close(); + _$markArticleAsUnreadEvent.close(); + _$starArticleEvent.close(); + _$unstarArticleEvent.close(); + _compositeSubscription.dispose(); + super.dispose(); + } +} diff --git a/packages/neon/lib/src/apps/news/blocs/articles.dart b/packages/neon/lib/src/apps/news/blocs/articles.dart index ca5c78f2..02fa328b 100644 --- a/packages/neon/lib/src/apps/news/blocs/articles.dart +++ b/packages/neon/lib/src/apps/news/blocs/articles.dart @@ -39,8 +39,6 @@ abstract class NewsArticlesBlocStates { BehaviorSubject get filterType; - Stream get articleUpdate; - Stream get errors; } @@ -71,36 +69,24 @@ class NewsArticlesBloc extends $NewsArticlesBloc { _$markArticleAsReadEvent.listen((final article) { _wrapArticleAction((final client) async { await client.news.markArticleAsRead(itemId: article.id); - // TODO - //_articleUpdateController.add(article..unread = false); }); }); _$markArticleAsUnreadEvent.listen((final article) { _wrapArticleAction((final client) async { await client.news.markArticleAsUnread(itemId: article.id); - // TODO - //_articleUpdateController.add(article..unread = true); }); }); _$starArticleEvent.listen((final article) { _wrapArticleAction((final client) async { - await client.news.starArticle( - itemId: article.id, - ); - // TODO - //_articleUpdateController.add(article..starred = true); + await client.news.starArticle(itemId: article.id); }); }); _$unstarArticleEvent.listen((final article) { _wrapArticleAction((final client) async { - await client.news.unstarArticle( - itemId: article.id, - ); - // TODO - //_articleUpdateController.add(article..starred = false); + await client.news.unstarArticle(itemId: article.id); }); }); @@ -184,14 +170,12 @@ class NewsArticlesBloc extends $NewsArticlesBloc { final _articlesSubject = BehaviorSubject>>(); late final BehaviorSubject _filterTypeSubject; - final _articleUpdateController = StreamController(); final _errorsStreamController = StreamController(); @override void dispose() { unawaited(_articlesSubject.close()); unawaited(_filterTypeSubject.close()); - unawaited(_articleUpdateController.close()); unawaited(_errorsStreamController.close()); super.dispose(); } @@ -202,9 +186,6 @@ class NewsArticlesBloc extends $NewsArticlesBloc { @override BehaviorSubject _mapToFilterTypeState() => _filterTypeSubject; - @override - Stream _mapToArticleUpdateState() => _articleUpdateController.stream.asBroadcastStream(); - @override Stream _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream(); } diff --git a/packages/neon/lib/src/apps/news/blocs/articles.rxb.g.dart b/packages/neon/lib/src/apps/news/blocs/articles.rxb.g.dart index e3184e4a..97ec8280 100644 --- a/packages/neon/lib/src/apps/news/blocs/articles.rxb.g.dart +++ b/packages/neon/lib/src/apps/news/blocs/articles.rxb.g.dart @@ -43,9 +43,6 @@ abstract class $NewsArticlesBloc extends RxBlocBase /// The state of [filterType] implemented in [_mapToFilterTypeState] late final BehaviorSubject _filterTypeState = _mapToFilterTypeState(); - /// The state of [articleUpdate] implemented in [_mapToArticleUpdateState] - late final Stream _articleUpdateState = _mapToArticleUpdateState(); - /// The state of [errors] implemented in [_mapToErrorsState] late final Stream _errorsState = _mapToErrorsState(); @@ -73,9 +70,6 @@ abstract class $NewsArticlesBloc extends RxBlocBase @override BehaviorSubject get filterType => _filterTypeState; - @override - Stream get articleUpdate => _articleUpdateState; - @override Stream get errors => _errorsState; @@ -83,8 +77,6 @@ abstract class $NewsArticlesBloc extends RxBlocBase BehaviorSubject _mapToFilterTypeState(); - Stream _mapToArticleUpdateState(); - Stream _mapToErrorsState(); @override diff --git a/packages/neon/lib/src/apps/news/pages/article.dart b/packages/neon/lib/src/apps/news/pages/article.dart index c2a9aec5..15cf2410 100644 --- a/packages/neon/lib/src/apps/news/pages/article.dart +++ b/packages/neon/lib/src/apps/news/pages/article.dart @@ -3,14 +3,14 @@ part of '../app.dart'; class NewsArticlePage extends StatefulWidget { const NewsArticlePage({ required this.bloc, - required this.article, + required this.articlesBloc, required this.useWebView, this.bodyData, super.key, }) : assert(useWebView || bodyData != null, 'bodyData has to be set when not using a WebView'); - final NewsArticlesBloc bloc; - final NewsArticle article; + final NewsArticleBloc bloc; + final NewsArticlesBloc articlesBloc; final bool useWebView; final String? bodyData; @@ -19,8 +19,6 @@ class NewsArticlePage extends StatefulWidget { } class _NewsArticlePageState extends State { - late NewsArticle article = widget.article; - bool _webviewLoading = true; WebViewController? _webviewController; Timer? _markAsReadTimer; @@ -29,12 +27,8 @@ class _NewsArticlePageState extends State { void initState() { super.initState(); - widget.bloc.articleUpdate.listen((final a) { - if (mounted && a.id == article.id) { - setState(() { - article = a; - }); - } + widget.bloc.errors.listen((final error) { + ExceptionWidget.showSnackbar(context, error); }); WidgetsBinding.instance.addPostFrameCallback((final _) async { @@ -44,7 +38,7 @@ class _NewsArticlePageState extends State { }); if (!widget.useWebView) { - _startMarkAsReadTimer(); + unawaited(_startMarkAsReadTimer()); } } @@ -55,14 +49,14 @@ class _NewsArticlePageState extends State { super.dispose(); } - void _startMarkAsReadTimer() { - if (article.unread) { - if (widget.bloc.newsBloc.options.articleDisableMarkAsReadTimeoutOption.value) { - widget.bloc.markArticleAsRead(article); + Future _startMarkAsReadTimer() async { + if (await widget.bloc.unread.first) { + if (widget.articlesBloc.newsBloc.options.articleDisableMarkAsReadTimeoutOption.value) { + widget.bloc.markArticleAsRead(); } else { - _markAsReadTimer = Timer(const Duration(seconds: 3), () { - if (article.unread) { - widget.bloc.markArticleAsRead(article); + _markAsReadTimer = Timer(const Duration(seconds: 3), () async { + if (await widget.bloc.unread.first) { + widget.bloc.markArticleAsRead(); } }); } @@ -81,7 +75,7 @@ class _NewsArticlePageState extends State { return (await _webviewController!.currentUrl())!; } - return article.url; + return widget.bloc.url; } @override @@ -101,25 +95,39 @@ class _NewsArticlePageState extends State { resizeToAvoidBottomInset: false, appBar: AppBar( actions: [ - IconButton( - onPressed: () async { - if (article.starred) { - widget.bloc.unstarArticle(article); - } else { - widget.bloc.starArticle(article); - } + RxBlocBuilder( + bloc: widget.bloc, + state: (final bloc) => bloc.starred, + builder: (final context, final starredSnapshot, final _) { + final starred = starredSnapshot.data ?? false; + return IconButton( + onPressed: () async { + if (starred) { + widget.bloc.unstarArticle(); + } else { + widget.bloc.starArticle(); + } + }, + icon: Icon(starred ? Icons.star : Icons.star_outline), + ); }, - icon: Icon(article.starred ? Icons.star : Icons.star_outline), ), - IconButton( - onPressed: () async { - if (article.unread) { - widget.bloc.markArticleAsRead(article); - } else { - widget.bloc.markArticleAsUnread(article); - } + RxBlocBuilder( + bloc: widget.bloc, + state: (final bloc) => bloc.unread, + builder: (final context, final unreadSnapshot, final _) { + final unread = unreadSnapshot.data ?? false; + return IconButton( + onPressed: () async { + if (unread) { + widget.bloc.markArticleAsRead(); + } else { + widget.bloc.markArticleAsUnread(); + } + }, + icon: Icon(unread ? MdiIcons.email : MdiIcons.emailMarkAsUnread), + ); }, - icon: Icon(article.unread ? MdiIcons.email : MdiIcons.emailMarkAsUnread), ), IconButton( onPressed: () async { @@ -147,15 +155,15 @@ class _NewsArticlePageState extends State { javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: (final controller) async { _webviewController = controller; - await controller.loadUrl(article.url); + await controller.loadUrl(widget.bloc.url); }, onPageStarted: (final _) { setState(() { _webviewLoading = true; }); }, - onPageFinished: (final _) { - _startMarkAsReadTimer(); + onPageFinished: (final _) async { + await _startMarkAsReadTimer(); setState(() { _webviewLoading = false; }); diff --git a/packages/neon/lib/src/apps/news/widgets/articles_view.dart b/packages/neon/lib/src/apps/news/widgets/articles_view.dart index f15a990e..ffaa3317 100644 --- a/packages/neon/lib/src/apps/news/widgets/articles_view.dart +++ b/packages/neon/lib/src/apps/news/widgets/articles_view.dart @@ -222,8 +222,13 @@ class _NewsArticlesViewState extends State { await Navigator.of(context).push( MaterialPageRoute( builder: (final context) => NewsArticlePage( - bloc: bloc, - article: article, + bloc: NewsArticleBloc( + Provider.of(context, listen: false), + RxBlocProvider.of(context).activeAccount.value!.client, + widget.bloc, + article, + ), + articlesBloc: widget.bloc, useWebView: false, bodyData: bodyData, ), @@ -234,8 +239,13 @@ class _NewsArticlesViewState extends State { await Navigator.of(context).push( MaterialPageRoute( builder: (final context) => NewsArticlePage( - bloc: bloc, - article: article, + bloc: NewsArticleBloc( + Provider.of(context, listen: false), + RxBlocProvider.of(context).activeAccount.value!.client, + widget.bloc, + article, + ), + articlesBloc: widget.bloc, useWebView: true, ), ),