Browse Source

neon: Implement proper article bloc

pull/112/head
jld3103 2 years ago
parent
commit
ec036772cd
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 2
      packages/neon/lib/src/apps/news/app.dart
  2. 103
      packages/neon/lib/src/apps/news/blocs/article.dart
  3. 85
      packages/neon/lib/src/apps/news/blocs/article.rxb.g.dart
  4. 23
      packages/neon/lib/src/apps/news/blocs/articles.dart
  5. 8
      packages/neon/lib/src/apps/news/blocs/articles.rxb.g.dart
  6. 86
      packages/neon/lib/src/apps/news/pages/article.dart
  7. 18
      packages/neon/lib/src/apps/news/widgets/articles_view.dart

2
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:html/parser.dart' as html_parser;
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:neon/l10n/localizations.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/articles.dart';
import 'package:neon/src/apps/news/blocs/news.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/blocs/apps.dart';
import 'package:neon/src/neon.dart'; import 'package:neon/src/neon.dart';
import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/nextcloud.dart';

103
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<bool> get unread;
BehaviorSubject<bool> get starred;
Stream<Exception> 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<bool>();
final _starredSubject = BehaviorSubject<bool>();
final _errorsStreamController = StreamController<Exception>();
@override
void dispose() {
unawaited(_unreadSubject.close());
unawaited(_starredSubject.close());
unawaited(_errorsStreamController.close());
super.dispose();
}
@override
BehaviorSubject<bool> _mapToUnreadState() => _unreadSubject;
@override
BehaviorSubject<bool> _mapToStarredState() => _starredSubject;
@override
Stream<Exception> _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream();
}

85
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<void>();
/// Тhe [Subject] where events sink to by calling [markArticleAsUnread]
final _$markArticleAsUnreadEvent = PublishSubject<void>();
/// Тhe [Subject] where events sink to by calling [starArticle]
final _$starArticleEvent = PublishSubject<void>();
/// Тhe [Subject] where events sink to by calling [unstarArticle]
final _$unstarArticleEvent = PublishSubject<void>();
/// The state of [unread] implemented in [_mapToUnreadState]
late final BehaviorSubject<bool> _unreadState = _mapToUnreadState();
/// The state of [starred] implemented in [_mapToStarredState]
late final BehaviorSubject<bool> _starredState = _mapToStarredState();
/// The state of [errors] implemented in [_mapToErrorsState]
late final Stream<Exception> _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<bool> get unread => _unreadState;
@override
BehaviorSubject<bool> get starred => _starredState;
@override
Stream<Exception> get errors => _errorsState;
BehaviorSubject<bool> _mapToUnreadState();
BehaviorSubject<bool> _mapToStarredState();
Stream<Exception> _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();
}
}

23
packages/neon/lib/src/apps/news/blocs/articles.dart

@ -39,8 +39,6 @@ abstract class NewsArticlesBlocStates {
BehaviorSubject<FilterType> get filterType; BehaviorSubject<FilterType> get filterType;
Stream<NewsArticle> get articleUpdate;
Stream<Exception> get errors; Stream<Exception> get errors;
} }
@ -71,36 +69,24 @@ class NewsArticlesBloc extends $NewsArticlesBloc {
_$markArticleAsReadEvent.listen((final article) { _$markArticleAsReadEvent.listen((final article) {
_wrapArticleAction((final client) async { _wrapArticleAction((final client) async {
await client.news.markArticleAsRead(itemId: article.id); await client.news.markArticleAsRead(itemId: article.id);
// TODO
//_articleUpdateController.add(article..unread = false);
}); });
}); });
_$markArticleAsUnreadEvent.listen((final article) { _$markArticleAsUnreadEvent.listen((final article) {
_wrapArticleAction((final client) async { _wrapArticleAction((final client) async {
await client.news.markArticleAsUnread(itemId: article.id); await client.news.markArticleAsUnread(itemId: article.id);
// TODO
//_articleUpdateController.add(article..unread = true);
}); });
}); });
_$starArticleEvent.listen((final article) { _$starArticleEvent.listen((final article) {
_wrapArticleAction((final client) async { _wrapArticleAction((final client) async {
await client.news.starArticle( await client.news.starArticle(itemId: article.id);
itemId: article.id,
);
// TODO
//_articleUpdateController.add(article..starred = true);
}); });
}); });
_$unstarArticleEvent.listen((final article) { _$unstarArticleEvent.listen((final article) {
_wrapArticleAction((final client) async { _wrapArticleAction((final client) async {
await client.news.unstarArticle( await client.news.unstarArticle(itemId: article.id);
itemId: article.id,
);
// TODO
//_articleUpdateController.add(article..starred = false);
}); });
}); });
@ -184,14 +170,12 @@ class NewsArticlesBloc extends $NewsArticlesBloc {
final _articlesSubject = BehaviorSubject<Result<List<NewsArticle>>>(); final _articlesSubject = BehaviorSubject<Result<List<NewsArticle>>>();
late final BehaviorSubject<FilterType> _filterTypeSubject; late final BehaviorSubject<FilterType> _filterTypeSubject;
final _articleUpdateController = StreamController<NewsArticle>();
final _errorsStreamController = StreamController<Exception>(); final _errorsStreamController = StreamController<Exception>();
@override @override
void dispose() { void dispose() {
unawaited(_articlesSubject.close()); unawaited(_articlesSubject.close());
unawaited(_filterTypeSubject.close()); unawaited(_filterTypeSubject.close());
unawaited(_articleUpdateController.close());
unawaited(_errorsStreamController.close()); unawaited(_errorsStreamController.close());
super.dispose(); super.dispose();
} }
@ -202,9 +186,6 @@ class NewsArticlesBloc extends $NewsArticlesBloc {
@override @override
BehaviorSubject<FilterType> _mapToFilterTypeState() => _filterTypeSubject; BehaviorSubject<FilterType> _mapToFilterTypeState() => _filterTypeSubject;
@override
Stream<NewsArticle> _mapToArticleUpdateState() => _articleUpdateController.stream.asBroadcastStream();
@override @override
Stream<Exception> _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream(); Stream<Exception> _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream();
} }

8
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] /// The state of [filterType] implemented in [_mapToFilterTypeState]
late final BehaviorSubject<FilterType> _filterTypeState = _mapToFilterTypeState(); late final BehaviorSubject<FilterType> _filterTypeState = _mapToFilterTypeState();
/// The state of [articleUpdate] implemented in [_mapToArticleUpdateState]
late final Stream<NewsArticle> _articleUpdateState = _mapToArticleUpdateState();
/// The state of [errors] implemented in [_mapToErrorsState] /// The state of [errors] implemented in [_mapToErrorsState]
late final Stream<Exception> _errorsState = _mapToErrorsState(); late final Stream<Exception> _errorsState = _mapToErrorsState();
@ -73,9 +70,6 @@ abstract class $NewsArticlesBloc extends RxBlocBase
@override @override
BehaviorSubject<FilterType> get filterType => _filterTypeState; BehaviorSubject<FilterType> get filterType => _filterTypeState;
@override
Stream<NewsArticle> get articleUpdate => _articleUpdateState;
@override @override
Stream<Exception> get errors => _errorsState; Stream<Exception> get errors => _errorsState;
@ -83,8 +77,6 @@ abstract class $NewsArticlesBloc extends RxBlocBase
BehaviorSubject<FilterType> _mapToFilterTypeState(); BehaviorSubject<FilterType> _mapToFilterTypeState();
Stream<NewsArticle> _mapToArticleUpdateState();
Stream<Exception> _mapToErrorsState(); Stream<Exception> _mapToErrorsState();
@override @override

86
packages/neon/lib/src/apps/news/pages/article.dart

@ -3,14 +3,14 @@ part of '../app.dart';
class NewsArticlePage extends StatefulWidget { class NewsArticlePage extends StatefulWidget {
const NewsArticlePage({ const NewsArticlePage({
required this.bloc, required this.bloc,
required this.article, required this.articlesBloc,
required this.useWebView, required this.useWebView,
this.bodyData, this.bodyData,
super.key, super.key,
}) : assert(useWebView || bodyData != null, 'bodyData has to be set when not using a WebView'); }) : assert(useWebView || bodyData != null, 'bodyData has to be set when not using a WebView');
final NewsArticlesBloc bloc; final NewsArticleBloc bloc;
final NewsArticle article; final NewsArticlesBloc articlesBloc;
final bool useWebView; final bool useWebView;
final String? bodyData; final String? bodyData;
@ -19,8 +19,6 @@ class NewsArticlePage extends StatefulWidget {
} }
class _NewsArticlePageState extends State<NewsArticlePage> { class _NewsArticlePageState extends State<NewsArticlePage> {
late NewsArticle article = widget.article;
bool _webviewLoading = true; bool _webviewLoading = true;
WebViewController? _webviewController; WebViewController? _webviewController;
Timer? _markAsReadTimer; Timer? _markAsReadTimer;
@ -29,12 +27,8 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
void initState() { void initState() {
super.initState(); super.initState();
widget.bloc.articleUpdate.listen((final a) { widget.bloc.errors.listen((final error) {
if (mounted && a.id == article.id) { ExceptionWidget.showSnackbar(context, error);
setState(() {
article = a;
});
}
}); });
WidgetsBinding.instance.addPostFrameCallback((final _) async { WidgetsBinding.instance.addPostFrameCallback((final _) async {
@ -44,7 +38,7 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
}); });
if (!widget.useWebView) { if (!widget.useWebView) {
_startMarkAsReadTimer(); unawaited(_startMarkAsReadTimer());
} }
} }
@ -55,14 +49,14 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
super.dispose(); super.dispose();
} }
void _startMarkAsReadTimer() { Future _startMarkAsReadTimer() async {
if (article.unread) { if (await widget.bloc.unread.first) {
if (widget.bloc.newsBloc.options.articleDisableMarkAsReadTimeoutOption.value) { if (widget.articlesBloc.newsBloc.options.articleDisableMarkAsReadTimeoutOption.value) {
widget.bloc.markArticleAsRead(article); widget.bloc.markArticleAsRead();
} else { } else {
_markAsReadTimer = Timer(const Duration(seconds: 3), () { _markAsReadTimer = Timer(const Duration(seconds: 3), () async {
if (article.unread) { if (await widget.bloc.unread.first) {
widget.bloc.markArticleAsRead(article); widget.bloc.markArticleAsRead();
} }
}); });
} }
@ -81,7 +75,7 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
return (await _webviewController!.currentUrl())!; return (await _webviewController!.currentUrl())!;
} }
return article.url; return widget.bloc.url;
} }
@override @override
@ -101,25 +95,39 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
actions: [ actions: [
IconButton( RxBlocBuilder(
onPressed: () async { bloc: widget.bloc,
if (article.starred) { state: (final bloc) => bloc.starred,
widget.bloc.unstarArticle(article); builder: (final context, final starredSnapshot, final _) {
} else { final starred = starredSnapshot.data ?? false;
widget.bloc.starArticle(article); 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( RxBlocBuilder(
onPressed: () async { bloc: widget.bloc,
if (article.unread) { state: (final bloc) => bloc.unread,
widget.bloc.markArticleAsRead(article); builder: (final context, final unreadSnapshot, final _) {
} else { final unread = unreadSnapshot.data ?? false;
widget.bloc.markArticleAsUnread(article); 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( IconButton(
onPressed: () async { onPressed: () async {
@ -147,15 +155,15 @@ class _NewsArticlePageState extends State<NewsArticlePage> {
javascriptMode: JavascriptMode.unrestricted, javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (final controller) async { onWebViewCreated: (final controller) async {
_webviewController = controller; _webviewController = controller;
await controller.loadUrl(article.url); await controller.loadUrl(widget.bloc.url);
}, },
onPageStarted: (final _) { onPageStarted: (final _) {
setState(() { setState(() {
_webviewLoading = true; _webviewLoading = true;
}); });
}, },
onPageFinished: (final _) { onPageFinished: (final _) async {
_startMarkAsReadTimer(); await _startMarkAsReadTimer();
setState(() { setState(() {
_webviewLoading = false; _webviewLoading = false;
}); });

18
packages/neon/lib/src/apps/news/widgets/articles_view.dart

@ -222,8 +222,13 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (final context) => NewsArticlePage( builder: (final context) => NewsArticlePage(
bloc: bloc, bloc: NewsArticleBloc(
article: article, Provider.of<RequestManager>(context, listen: false),
RxBlocProvider.of<AccountsBloc>(context).activeAccount.value!.client,
widget.bloc,
article,
),
articlesBloc: widget.bloc,
useWebView: false, useWebView: false,
bodyData: bodyData, bodyData: bodyData,
), ),
@ -234,8 +239,13 @@ class _NewsArticlesViewState extends State<NewsArticlesView> {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (final context) => NewsArticlePage( builder: (final context) => NewsArticlePage(
bloc: bloc, bloc: NewsArticleBloc(
article: article, Provider.of<RequestManager>(context, listen: false),
RxBlocProvider.of<AccountsBloc>(context).activeAccount.value!.client,
widget.bloc,
article,
),
articlesBloc: widget.bloc,
useWebView: true, useWebView: true,
), ),
), ),

Loading…
Cancel
Save