From ec036772cd93de75a4463b07843cf54242612bef Mon Sep 17 00:00:00 2001 From: jld3103 Date: Thu, 27 Oct 2022 18:58:12 +0200 Subject: [PATCH 1/2] neon: Implement proper article bloc --- packages/neon/lib/src/apps/news/app.dart | 2 + .../neon/lib/src/apps/news/blocs/article.dart | 103 ++++++++++++++++++ .../src/apps/news/blocs/article.rxb.g.dart | 85 +++++++++++++++ .../lib/src/apps/news/blocs/articles.dart | 23 +--- .../src/apps/news/blocs/articles.rxb.g.dart | 8 -- .../neon/lib/src/apps/news/pages/article.dart | 86 ++++++++------- .../src/apps/news/widgets/articles_view.dart | 18 ++- 7 files changed, 253 insertions(+), 72 deletions(-) create mode 100644 packages/neon/lib/src/apps/news/blocs/article.dart create mode 100644 packages/neon/lib/src/apps/news/blocs/article.rxb.g.dart 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, ), ), From 0612274b22141afa56df09c01766a5f516ec7b99 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Thu, 27 Oct 2022 19:32:23 +0200 Subject: [PATCH 2/2] neon: Implement proper note bloc --- packages/neon/lib/src/apps/notes/app.dart | 4 + .../neon/lib/src/apps/notes/blocs/note.dart | 116 +++++++++++++++ .../lib/src/apps/notes/blocs/note.rxb.g.dart | 120 +++++++++++++++ .../neon/lib/src/apps/notes/blocs/notes.dart | 22 +-- .../lib/src/apps/notes/blocs/notes.rxb.g.dart | 8 - .../apps/notes/dialogs/select_category.dart | 8 +- .../neon/lib/src/apps/notes/pages/note.dart | 137 ++++++++++-------- .../src/apps/notes/widgets/notes_view.dart | 10 +- 8 files changed, 335 insertions(+), 90 deletions(-) create mode 100644 packages/neon/lib/src/apps/notes/blocs/note.dart create mode 100644 packages/neon/lib/src/apps/notes/blocs/note.rxb.g.dart diff --git a/packages/neon/lib/src/apps/notes/app.dart b/packages/neon/lib/src/apps/notes/app.dart index 2a82b692..002e7943 100644 --- a/packages/neon/lib/src/apps/notes/app.dart +++ b/packages/neon/lib/src/apps/notes/app.dart @@ -1,13 +1,17 @@ library notes; +import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_rx_bloc/flutter_rx_bloc.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:neon/l10n/localizations.dart'; +import 'package:neon/src/apps/notes/blocs/note.dart'; import 'package:neon/src/apps/notes/blocs/notes.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/notes/blocs/note.dart b/packages/neon/lib/src/apps/notes/blocs/note.dart new file mode 100644 index 00000000..44426cd3 --- /dev/null +++ b/packages/neon/lib/src/apps/notes/blocs/note.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:neon/src/apps/notes/app.dart'; +import 'package:neon/src/apps/notes/blocs/notes.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 'note.rxb.g.dart'; + +abstract class NotesNoteBlocEvents { + void updateNote( + final int id, + final String etag, { + final String? title, + final String? category, + final String? content, + final bool? favorite, + }); +} + +abstract class NotesNoteBlocStates { + BehaviorSubject get content; + + BehaviorSubject get title; + + BehaviorSubject get category; + + BehaviorSubject get etag; + + Stream get errors; +} + +@RxBloc() +class NotesNoteBloc extends $NotesNoteBloc { + NotesNoteBloc( + this.options, + this._requestManager, + this._client, + this._notesBloc, + final NotesNote note, + ) { + _$updateNoteEvent.listen((final event) { + _wrapAction( + () async { + _emitNote( + await _client.notes.updateNote( + id: event.id, + title: event.title, + category: event.category, + content: event.content, + favorite: event.favorite ?? false ? 1 : 0, + ifMatch: '"${event.etag}"', + ), + ); + }, + ); + }); + + _emitNote(note); + id = note.id; + } + + void _emitNote(final NotesNote note) { + _contentSubject.add(note.content); + _titleSubject.add(note.title); + _categorySubject.add(note.category); + _etagSubject.add(note.etag); + } + + void _wrapAction(final Future Function() call) { + final stream = _requestManager.wrapWithoutCache(call).asBroadcastStream(); + stream.whereError().listen(_errorsStreamController.add); + stream.whereSuccess().listen((final _) async { + _notesBloc.refresh(); + }); + } + + final NotesAppSpecificOptions options; + final RequestManager _requestManager; + final NextcloudClient _client; + final NotesBloc _notesBloc; + + late final int id; + final _contentSubject = BehaviorSubject(); + final _titleSubject = BehaviorSubject(); + final _categorySubject = BehaviorSubject(); + final _etagSubject = BehaviorSubject(); + final _errorsStreamController = StreamController(); + + @override + void dispose() { + unawaited(_contentSubject.close()); + unawaited(_titleSubject.close()); + unawaited(_categorySubject.close()); + unawaited(_etagSubject.close()); + unawaited(_errorsStreamController.close()); + super.dispose(); + } + + @override + Stream _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream(); + + @override + BehaviorSubject _mapToContentState() => _contentSubject; + + @override + BehaviorSubject _mapToTitleState() => _titleSubject; + + @override + BehaviorSubject _mapToCategoryState() => _categorySubject; + + @override + BehaviorSubject _mapToEtagState() => _etagSubject; +} diff --git a/packages/neon/lib/src/apps/notes/blocs/note.rxb.g.dart b/packages/neon/lib/src/apps/notes/blocs/note.rxb.g.dart new file mode 100644 index 00000000..6daf4dba --- /dev/null +++ b/packages/neon/lib/src/apps/notes/blocs/note.rxb.g.dart @@ -0,0 +1,120 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Generator: RxBlocGeneratorForAnnotation +// ************************************************************************** + +part of 'note.dart'; + +/// Used as a contractor for the bloc, events and states classes +/// {@nodoc} +abstract class NotesNoteBlocType extends RxBlocTypeBase { + NotesNoteBlocEvents get events; + NotesNoteBlocStates get states; +} + +/// [$NotesNoteBloc] extended by the [NotesNoteBloc] +/// {@nodoc} +abstract class $NotesNoteBloc extends RxBlocBase + implements NotesNoteBlocEvents, NotesNoteBlocStates, NotesNoteBlocType { + final _compositeSubscription = CompositeSubscription(); + + /// Тhe [Subject] where events sink to by calling [updateNote] + final _$updateNoteEvent = PublishSubject<_UpdateNoteEventArgs>(); + + /// The state of [content] implemented in [_mapToContentState] + late final BehaviorSubject _contentState = _mapToContentState(); + + /// The state of [title] implemented in [_mapToTitleState] + late final BehaviorSubject _titleState = _mapToTitleState(); + + /// The state of [category] implemented in [_mapToCategoryState] + late final BehaviorSubject _categoryState = _mapToCategoryState(); + + /// The state of [etag] implemented in [_mapToEtagState] + late final BehaviorSubject _etagState = _mapToEtagState(); + + /// The state of [errors] implemented in [_mapToErrorsState] + late final Stream _errorsState = _mapToErrorsState(); + + @override + void updateNote( + int id, + String etag, { + String? title, + String? category, + String? content, + bool? favorite, + }) => + _$updateNoteEvent.add(_UpdateNoteEventArgs( + id, + etag, + title: title, + category: category, + content: content, + favorite: favorite, + )); + + @override + BehaviorSubject get content => _contentState; + + @override + BehaviorSubject get title => _titleState; + + @override + BehaviorSubject get category => _categoryState; + + @override + BehaviorSubject get etag => _etagState; + + @override + Stream get errors => _errorsState; + + BehaviorSubject _mapToContentState(); + + BehaviorSubject _mapToTitleState(); + + BehaviorSubject _mapToCategoryState(); + + BehaviorSubject _mapToEtagState(); + + Stream _mapToErrorsState(); + + @override + NotesNoteBlocEvents get events => this; + + @override + NotesNoteBlocStates get states => this; + + @override + void dispose() { + _$updateNoteEvent.close(); + _compositeSubscription.dispose(); + super.dispose(); + } +} + +/// Helps providing the arguments in the [Subject.add] for +/// [NotesNoteBlocEvents.updateNote] event +class _UpdateNoteEventArgs { + const _UpdateNoteEventArgs( + this.id, + this.etag, { + this.title, + this.category, + this.content, + this.favorite, + }); + + final int id; + + final String etag; + + final String? title; + + final String? category; + + final String? content; + + final bool? favorite; +} diff --git a/packages/neon/lib/src/apps/notes/blocs/notes.dart b/packages/neon/lib/src/apps/notes/blocs/notes.dart index d12071bc..796e86da 100644 --- a/packages/neon/lib/src/apps/notes/blocs/notes.dart +++ b/packages/neon/lib/src/apps/notes/blocs/notes.dart @@ -32,8 +32,6 @@ abstract class NotesBlocEvents { abstract class NotesBlocStates { BehaviorSubject>> get notes; - Stream get noteUpdate; - Stream get errors; } @@ -57,15 +55,13 @@ class NotesBloc extends $NotesBloc { _$updateNoteEvent.listen((final event) { _wrapAction( - () async => _noteUpdateController.add( - await client.notes.updateNote( - id: event.id, - title: event.title, - category: event.category, - content: event.content, - favorite: event.favorite ?? false ? 1 : 0, - ifMatch: '"${event.etag}"', - ), + () async => client.notes.updateNote( + id: event.id, + title: event.title, + category: event.category, + content: event.content, + favorite: event.favorite ?? false ? 1 : 0, + ifMatch: '"${event.etag}"', ), ); }); @@ -102,7 +98,6 @@ class NotesBloc extends $NotesBloc { final NextcloudClient client; final _notesSubject = BehaviorSubject>>(); - final _noteUpdateController = StreamController(); final _errorsStreamController = StreamController(); @override @@ -115,9 +110,6 @@ class NotesBloc extends $NotesBloc { @override BehaviorSubject>> _mapToNotesState() => _notesSubject; - @override - Stream _mapToNoteUpdateState() => _noteUpdateController.stream.asBroadcastStream(); - @override Stream _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream(); } diff --git a/packages/neon/lib/src/apps/notes/blocs/notes.rxb.g.dart b/packages/neon/lib/src/apps/notes/blocs/notes.rxb.g.dart index 278dae37..7c376d26 100644 --- a/packages/neon/lib/src/apps/notes/blocs/notes.rxb.g.dart +++ b/packages/neon/lib/src/apps/notes/blocs/notes.rxb.g.dart @@ -33,9 +33,6 @@ abstract class $NotesBloc extends RxBlocBase implements NotesBlocEvents, NotesBl /// The state of [notes] implemented in [_mapToNotesState] late final BehaviorSubject>> _notesState = _mapToNotesState(); - /// The state of [noteUpdate] implemented in [_mapToNoteUpdateState] - late final Stream _noteUpdateState = _mapToNoteUpdateState(); - /// The state of [errors] implemented in [_mapToErrorsState] late final Stream _errorsState = _mapToErrorsState(); @@ -76,16 +73,11 @@ abstract class $NotesBloc extends RxBlocBase implements NotesBlocEvents, NotesBl @override BehaviorSubject>> get notes => _notesState; - @override - Stream get noteUpdate => _noteUpdateState; - @override Stream get errors => _errorsState; BehaviorSubject>> _mapToNotesState(); - Stream _mapToNoteUpdateState(); - Stream _mapToErrorsState(); @override diff --git a/packages/neon/lib/src/apps/notes/dialogs/select_category.dart b/packages/neon/lib/src/apps/notes/dialogs/select_category.dart index cfcc6612..c745b346 100644 --- a/packages/neon/lib/src/apps/notes/dialogs/select_category.dart +++ b/packages/neon/lib/src/apps/notes/dialogs/select_category.dart @@ -3,12 +3,12 @@ part of '../app.dart'; class NotesSelectCategoryDialog extends StatefulWidget { const NotesSelectCategoryDialog({ required this.bloc, - required this.note, + this.initialCategory, super.key, }); final NotesBloc bloc; - final NotesNote note; + final String? initialCategory; @override State createState() => _NotesSelectCategoryDialogState(); @@ -21,7 +21,7 @@ class _NotesSelectCategoryDialogState extends State { void submit() { if (formKey.currentState!.validate()) { - Navigator.of(context).pop(selectedCategory ?? widget.note.category); + Navigator.of(context).pop(selectedCategory); } } @@ -60,7 +60,7 @@ class _NotesSelectCategoryDialogState extends State { if (notesData != null) ...[ NotesCategorySelect( categories: notesData.map((final note) => note.category).toSet().toList(), - initialValue: widget.note.category, + initialValue: widget.initialCategory, onChanged: (final category) { selectedCategory = category; }, diff --git a/packages/neon/lib/src/apps/notes/pages/note.dart b/packages/neon/lib/src/apps/notes/pages/note.dart index 1e22f298..58d50490 100644 --- a/packages/neon/lib/src/apps/notes/pages/note.dart +++ b/packages/neon/lib/src/apps/notes/pages/note.dart @@ -3,41 +3,41 @@ part of '../app.dart'; class NotesNotePage extends StatefulWidget { const NotesNotePage({ required this.bloc, - required this.note, + required this.notesBloc, super.key, }); - final NotesBloc bloc; - final NotesNote note; + final NotesNoteBloc bloc; + final NotesBloc notesBloc; @override State createState() => _NotesNotePageState(); } class _NotesNotePageState extends State { - late final _contentController = TextEditingController(text: widget.note.content); - late final _titleController = TextEditingController(text: widget.note.title); + late final _contentController = TextEditingController(); + late final _titleController = TextEditingController(); final _contentFocusNode = FocusNode(); final _titleFocusNode = FocusNode(); - - late NotesNote _note = widget.note; + final _updateController = StreamController(); bool _showEditor = false; - bool _synced = true; void _focusEditor() { _contentFocusNode.requestFocus(); _contentController.selection = TextSelection.collapsed(offset: _contentController.text.length); } - void _update([final String? selectedCategory]) { - final updatedTitle = _note.title != _titleController.text ? _titleController.text : null; - final updatedCategory = selectedCategory != null && _note.category != selectedCategory ? selectedCategory : null; - final updatedContent = _note.content != _contentController.text ? _contentController.text : null; + Future _update({ + final String? category, + }) async { + final updatedTitle = await widget.bloc.title.first != _titleController.text ? _titleController.text : null; + final updatedCategory = category != null && await widget.bloc.category.first != category ? category : null; + final updatedContent = await widget.bloc.content.first != _contentController.text ? _contentController.text : null; if (updatedTitle != null || updatedCategory != null || updatedContent != null) { widget.bloc.updateNote( - _note.id, - _note.etag, + widget.bloc.id, + await widget.bloc.etag.first, title: updatedTitle, category: updatedCategory, content: updatedContent, @@ -49,33 +49,28 @@ class _NotesNotePageState extends State { void initState() { super.initState(); - void updateSynced() { - _synced = _note.content == _contentController.text; - } - - _contentController.addListener(() => setState(updateSynced)); + widget.bloc.errors.listen((final error) { + handleNotesException(context, error); + }); - widget.bloc.noteUpdate.listen((final n) { - if (mounted && n.id == _note.id) { - setState(() { - _note = n; - updateSynced(); - }); - } + widget.bloc.content.listen((final content) { + _contentController.text = content; }); - _titleFocusNode.addListener(() { - if (!_titleFocusNode.hasFocus) { - _update(); - } + widget.bloc.title.listen((final title) { + _titleController.text = title; }); + _contentController.addListener(() => _updateController.add(null)); + _titleController.addListener(() => _updateController.add(null)); + _updateController.stream.debounceTime(const Duration(seconds: 1)).listen((final _) async => _update()); + WidgetsBinding.instance.addPostFrameCallback((final _) async { if (Provider.of(context, listen: false).canUseWakelock) { await Wakelock.enable(); } if (widget.bloc.options.defaultNoteViewTypeOption.value == DefaultNoteViewType.edit || - widget.note.content.isEmpty) { + (await widget.bloc.content.first).isEmpty) { setState(() { _showEditor = true; }); @@ -84,11 +79,20 @@ class _NotesNotePageState extends State { }); } + @override + void dispose() { + unawaited(_updateController.close()); + super.dispose(); + } + @override Widget build(final BuildContext context) => WillPopScope( onWillPop: () async { - _update(); + await _update(); + if (!mounted) { + return true; + } if (Provider.of(context, listen: false).canUseWakelock) { await Wakelock.disable(); } @@ -112,12 +116,6 @@ class _NotesNotePageState extends State { ), ), actions: [ - IconButton( - icon: Icon( - _synced ? Icons.check : Icons.sync, - ), - onPressed: _update, - ), IconButton( icon: Icon( _showEditor ? Icons.visibility : Icons.edit, @@ -135,23 +133,33 @@ class _NotesNotePageState extends State { } }, ), - IconButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (final context) => NotesSelectCategoryDialog( - bloc: widget.bloc, - note: _note, + RxBlocBuilder( + bloc: widget.bloc, + state: (final bloc) => bloc.category, + builder: (final context, final categorySnapshot, final _) { + final category = categorySnapshot.data ?? ''; + + return IconButton( + onPressed: () async { + final result = await showDialog( + context: context, + builder: (final context) => NotesSelectCategoryDialog( + bloc: widget.notesBloc, + initialCategory: category, + ), + ); + if (result != null) { + await _update( + category: result, + ); + } + }, + icon: Icon( + MdiIcons.tag, + color: category.isNotEmpty ? NotesCategoryColor.compute(category) : null, ), ); - if (result != null) { - _update(result); - } }, - icon: Icon( - MdiIcons.tag, - color: _note.category.isNotEmpty ? NotesCategoryColor.compute(_note.category) : null, - ), ), ], ), @@ -178,15 +186,22 @@ class _NotesNotePageState extends State { border: InputBorder.none, ), ) - : MarkdownBody( - data: _contentController.text, - onTapLink: (final text, final href, final title) async { - if (href != null) { - await launchUrlString( - href, - mode: LaunchMode.externalApplication, - ); - } + : RxBlocBuilder( + bloc: widget.bloc, + state: (final bloc) => bloc.content, + builder: (final context, final contentSnapshot, final _) { + final content = contentSnapshot.data ?? ''; + return MarkdownBody( + data: content, + onTapLink: (final text, final href, final title) async { + if (href != null) { + await launchUrlString( + href, + mode: LaunchMode.externalApplication, + ); + } + }, + ); }, ), ), diff --git a/packages/neon/lib/src/apps/notes/widgets/notes_view.dart b/packages/neon/lib/src/apps/notes/widgets/notes_view.dart index 443d0fe4..1c71cc28 100644 --- a/packages/neon/lib/src/apps/notes/widgets/notes_view.dart +++ b/packages/neon/lib/src/apps/notes/widgets/notes_view.dart @@ -125,8 +125,14 @@ class NotesView extends StatelessWidget { await Navigator.of(context).push( MaterialPageRoute( builder: (final context) => NotesNotePage( - bloc: bloc, - note: note, + bloc: NotesNoteBloc( + bloc.options, + Provider.of(context, listen: false), + RxBlocProvider.of(context).activeAccount.value!.client, + bloc, + note, + ), + notesBloc: bloc, ), ), );