Browse Source

neon: Implement proper note bloc

pull/112/head
jld3103 2 years ago
parent
commit
0612274b22
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  1. 4
      packages/neon/lib/src/apps/notes/app.dart
  2. 116
      packages/neon/lib/src/apps/notes/blocs/note.dart
  3. 120
      packages/neon/lib/src/apps/notes/blocs/note.rxb.g.dart
  4. 22
      packages/neon/lib/src/apps/notes/blocs/notes.dart
  5. 8
      packages/neon/lib/src/apps/notes/blocs/notes.rxb.g.dart
  6. 8
      packages/neon/lib/src/apps/notes/dialogs/select_category.dart
  7. 137
      packages/neon/lib/src/apps/notes/pages/note.dart
  8. 10
      packages/neon/lib/src/apps/notes/widgets/notes_view.dart

4
packages/neon/lib/src/apps/notes/app.dart

@ -1,13 +1,17 @@
library notes; library notes;
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.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: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/notes/blocs/note.dart';
import 'package:neon/src/apps/notes/blocs/notes.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/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';

116
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<String> get content;
BehaviorSubject<String> get title;
BehaviorSubject<String> get category;
BehaviorSubject<String> get etag;
Stream<Exception> 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<String>();
final _titleSubject = BehaviorSubject<String>();
final _categorySubject = BehaviorSubject<String>();
final _etagSubject = BehaviorSubject<String>();
final _errorsStreamController = StreamController<Exception>();
@override
void dispose() {
unawaited(_contentSubject.close());
unawaited(_titleSubject.close());
unawaited(_categorySubject.close());
unawaited(_etagSubject.close());
unawaited(_errorsStreamController.close());
super.dispose();
}
@override
Stream<Exception> _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream();
@override
BehaviorSubject<String> _mapToContentState() => _contentSubject;
@override
BehaviorSubject<String> _mapToTitleState() => _titleSubject;
@override
BehaviorSubject<String> _mapToCategoryState() => _categorySubject;
@override
BehaviorSubject<String> _mapToEtagState() => _etagSubject;
}

120
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<String> _contentState = _mapToContentState();
/// The state of [title] implemented in [_mapToTitleState]
late final BehaviorSubject<String> _titleState = _mapToTitleState();
/// The state of [category] implemented in [_mapToCategoryState]
late final BehaviorSubject<String> _categoryState = _mapToCategoryState();
/// The state of [etag] implemented in [_mapToEtagState]
late final BehaviorSubject<String> _etagState = _mapToEtagState();
/// The state of [errors] implemented in [_mapToErrorsState]
late final Stream<Exception> _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<String> get content => _contentState;
@override
BehaviorSubject<String> get title => _titleState;
@override
BehaviorSubject<String> get category => _categoryState;
@override
BehaviorSubject<String> get etag => _etagState;
@override
Stream<Exception> get errors => _errorsState;
BehaviorSubject<String> _mapToContentState();
BehaviorSubject<String> _mapToTitleState();
BehaviorSubject<String> _mapToCategoryState();
BehaviorSubject<String> _mapToEtagState();
Stream<Exception> _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;
}

22
packages/neon/lib/src/apps/notes/blocs/notes.dart

@ -32,8 +32,6 @@ abstract class NotesBlocEvents {
abstract class NotesBlocStates { abstract class NotesBlocStates {
BehaviorSubject<Result<List<NotesNote>>> get notes; BehaviorSubject<Result<List<NotesNote>>> get notes;
Stream<NotesNote> get noteUpdate;
Stream<Exception> get errors; Stream<Exception> get errors;
} }
@ -57,15 +55,13 @@ class NotesBloc extends $NotesBloc {
_$updateNoteEvent.listen((final event) { _$updateNoteEvent.listen((final event) {
_wrapAction( _wrapAction(
() async => _noteUpdateController.add( () async => client.notes.updateNote(
await client.notes.updateNote( id: event.id,
id: event.id, title: event.title,
title: event.title, category: event.category,
category: event.category, content: event.content,
content: event.content, favorite: event.favorite ?? false ? 1 : 0,
favorite: event.favorite ?? false ? 1 : 0, ifMatch: '"${event.etag}"',
ifMatch: '"${event.etag}"',
),
), ),
); );
}); });
@ -102,7 +98,6 @@ class NotesBloc extends $NotesBloc {
final NextcloudClient client; final NextcloudClient client;
final _notesSubject = BehaviorSubject<Result<List<NotesNote>>>(); final _notesSubject = BehaviorSubject<Result<List<NotesNote>>>();
final _noteUpdateController = StreamController<NotesNote>();
final _errorsStreamController = StreamController<Exception>(); final _errorsStreamController = StreamController<Exception>();
@override @override
@ -115,9 +110,6 @@ class NotesBloc extends $NotesBloc {
@override @override
BehaviorSubject<Result<List<NotesNote>>> _mapToNotesState() => _notesSubject; BehaviorSubject<Result<List<NotesNote>>> _mapToNotesState() => _notesSubject;
@override
Stream<NotesNote> _mapToNoteUpdateState() => _noteUpdateController.stream.asBroadcastStream();
@override @override
Stream<Exception> _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream(); Stream<Exception> _mapToErrorsState() => _errorsStreamController.stream.asBroadcastStream();
} }

8
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] /// The state of [notes] implemented in [_mapToNotesState]
late final BehaviorSubject<Result<List<NotesNote>>> _notesState = _mapToNotesState(); late final BehaviorSubject<Result<List<NotesNote>>> _notesState = _mapToNotesState();
/// The state of [noteUpdate] implemented in [_mapToNoteUpdateState]
late final Stream<NotesNote> _noteUpdateState = _mapToNoteUpdateState();
/// 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();
@ -76,16 +73,11 @@ abstract class $NotesBloc extends RxBlocBase implements NotesBlocEvents, NotesBl
@override @override
BehaviorSubject<Result<List<NotesNote>>> get notes => _notesState; BehaviorSubject<Result<List<NotesNote>>> get notes => _notesState;
@override
Stream<NotesNote> get noteUpdate => _noteUpdateState;
@override @override
Stream<Exception> get errors => _errorsState; Stream<Exception> get errors => _errorsState;
BehaviorSubject<Result<List<NotesNote>>> _mapToNotesState(); BehaviorSubject<Result<List<NotesNote>>> _mapToNotesState();
Stream<NotesNote> _mapToNoteUpdateState();
Stream<Exception> _mapToErrorsState(); Stream<Exception> _mapToErrorsState();
@override @override

8
packages/neon/lib/src/apps/notes/dialogs/select_category.dart

@ -3,12 +3,12 @@ part of '../app.dart';
class NotesSelectCategoryDialog extends StatefulWidget { class NotesSelectCategoryDialog extends StatefulWidget {
const NotesSelectCategoryDialog({ const NotesSelectCategoryDialog({
required this.bloc, required this.bloc,
required this.note, this.initialCategory,
super.key, super.key,
}); });
final NotesBloc bloc; final NotesBloc bloc;
final NotesNote note; final String? initialCategory;
@override @override
State<NotesSelectCategoryDialog> createState() => _NotesSelectCategoryDialogState(); State<NotesSelectCategoryDialog> createState() => _NotesSelectCategoryDialogState();
@ -21,7 +21,7 @@ class _NotesSelectCategoryDialogState extends State<NotesSelectCategoryDialog> {
void submit() { void submit() {
if (formKey.currentState!.validate()) { 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<NotesSelectCategoryDialog> {
if (notesData != null) ...[ if (notesData != null) ...[
NotesCategorySelect( NotesCategorySelect(
categories: notesData.map((final note) => note.category).toSet().toList(), categories: notesData.map((final note) => note.category).toSet().toList(),
initialValue: widget.note.category, initialValue: widget.initialCategory,
onChanged: (final category) { onChanged: (final category) {
selectedCategory = category; selectedCategory = category;
}, },

137
packages/neon/lib/src/apps/notes/pages/note.dart

@ -3,41 +3,41 @@ part of '../app.dart';
class NotesNotePage extends StatefulWidget { class NotesNotePage extends StatefulWidget {
const NotesNotePage({ const NotesNotePage({
required this.bloc, required this.bloc,
required this.note, required this.notesBloc,
super.key, super.key,
}); });
final NotesBloc bloc; final NotesNoteBloc bloc;
final NotesNote note; final NotesBloc notesBloc;
@override @override
State<NotesNotePage> createState() => _NotesNotePageState(); State<NotesNotePage> createState() => _NotesNotePageState();
} }
class _NotesNotePageState extends State<NotesNotePage> { class _NotesNotePageState extends State<NotesNotePage> {
late final _contentController = TextEditingController(text: widget.note.content); late final _contentController = TextEditingController();
late final _titleController = TextEditingController(text: widget.note.title); late final _titleController = TextEditingController();
final _contentFocusNode = FocusNode(); final _contentFocusNode = FocusNode();
final _titleFocusNode = FocusNode(); final _titleFocusNode = FocusNode();
final _updateController = StreamController();
late NotesNote _note = widget.note;
bool _showEditor = false; bool _showEditor = false;
bool _synced = true;
void _focusEditor() { void _focusEditor() {
_contentFocusNode.requestFocus(); _contentFocusNode.requestFocus();
_contentController.selection = TextSelection.collapsed(offset: _contentController.text.length); _contentController.selection = TextSelection.collapsed(offset: _contentController.text.length);
} }
void _update([final String? selectedCategory]) { Future _update({
final updatedTitle = _note.title != _titleController.text ? _titleController.text : null; final String? category,
final updatedCategory = selectedCategory != null && _note.category != selectedCategory ? selectedCategory : null; }) async {
final updatedContent = _note.content != _contentController.text ? _contentController.text : null; 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) { if (updatedTitle != null || updatedCategory != null || updatedContent != null) {
widget.bloc.updateNote( widget.bloc.updateNote(
_note.id, widget.bloc.id,
_note.etag, await widget.bloc.etag.first,
title: updatedTitle, title: updatedTitle,
category: updatedCategory, category: updatedCategory,
content: updatedContent, content: updatedContent,
@ -49,33 +49,28 @@ class _NotesNotePageState extends State<NotesNotePage> {
void initState() { void initState() {
super.initState(); super.initState();
void updateSynced() { widget.bloc.errors.listen((final error) {
_synced = _note.content == _contentController.text; handleNotesException(context, error);
} });
_contentController.addListener(() => setState(updateSynced));
widget.bloc.noteUpdate.listen((final n) { widget.bloc.content.listen((final content) {
if (mounted && n.id == _note.id) { _contentController.text = content;
setState(() {
_note = n;
updateSynced();
});
}
}); });
_titleFocusNode.addListener(() { widget.bloc.title.listen((final title) {
if (!_titleFocusNode.hasFocus) { _titleController.text = title;
_update();
}
}); });
_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 { WidgetsBinding.instance.addPostFrameCallback((final _) async {
if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) { if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) {
await Wakelock.enable(); await Wakelock.enable();
} }
if (widget.bloc.options.defaultNoteViewTypeOption.value == DefaultNoteViewType.edit || if (widget.bloc.options.defaultNoteViewTypeOption.value == DefaultNoteViewType.edit ||
widget.note.content.isEmpty) { (await widget.bloc.content.first).isEmpty) {
setState(() { setState(() {
_showEditor = true; _showEditor = true;
}); });
@ -84,11 +79,20 @@ class _NotesNotePageState extends State<NotesNotePage> {
}); });
} }
@override
void dispose() {
unawaited(_updateController.close());
super.dispose();
}
@override @override
Widget build(final BuildContext context) => WillPopScope( Widget build(final BuildContext context) => WillPopScope(
onWillPop: () async { onWillPop: () async {
_update(); await _update();
if (!mounted) {
return true;
}
if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) { if (Provider.of<NeonPlatform>(context, listen: false).canUseWakelock) {
await Wakelock.disable(); await Wakelock.disable();
} }
@ -112,12 +116,6 @@ class _NotesNotePageState extends State<NotesNotePage> {
), ),
), ),
actions: [ actions: [
IconButton(
icon: Icon(
_synced ? Icons.check : Icons.sync,
),
onPressed: _update,
),
IconButton( IconButton(
icon: Icon( icon: Icon(
_showEditor ? Icons.visibility : Icons.edit, _showEditor ? Icons.visibility : Icons.edit,
@ -135,23 +133,33 @@ class _NotesNotePageState extends State<NotesNotePage> {
} }
}, },
), ),
IconButton( RxBlocBuilder(
onPressed: () async { bloc: widget.bloc,
final result = await showDialog<String>( state: (final bloc) => bloc.category,
context: context, builder: (final context, final categorySnapshot, final _) {
builder: (final context) => NotesSelectCategoryDialog( final category = categorySnapshot.data ?? '';
bloc: widget.bloc,
note: _note, return IconButton(
onPressed: () async {
final result = await showDialog<String>(
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<NotesNotePage> {
border: InputBorder.none, border: InputBorder.none,
), ),
) )
: MarkdownBody( : RxBlocBuilder(
data: _contentController.text, bloc: widget.bloc,
onTapLink: (final text, final href, final title) async { state: (final bloc) => bloc.content,
if (href != null) { builder: (final context, final contentSnapshot, final _) {
await launchUrlString( final content = contentSnapshot.data ?? '';
href, return MarkdownBody(
mode: LaunchMode.externalApplication, data: content,
); onTapLink: (final text, final href, final title) async {
} if (href != null) {
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}
},
);
}, },
), ),
), ),

10
packages/neon/lib/src/apps/notes/widgets/notes_view.dart

@ -125,8 +125,14 @@ class NotesView extends StatelessWidget {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (final context) => NotesNotePage( builder: (final context) => NotesNotePage(
bloc: bloc, bloc: NotesNoteBloc(
note: note, bloc.options,
Provider.of<RequestManager>(context, listen: false),
RxBlocProvider.of<AccountsBloc>(context).activeAccount.value!.client,
bloc,
note,
),
notesBloc: bloc,
), ),
), ),
); );

Loading…
Cancel
Save