();
@@ -196,7 +243,7 @@ class Note extends Equatable {
case SortMethod.title:
return sortAscending ? title.compareTo(otherNote.title) : otherNote.title.compareTo(title);
default:
- throw Exception();
+ throw Exception('The sort method is not valid: $sortMethod');
}
}
}
diff --git a/lib/models/note/note.g.dart b/lib/models/note/note.g.dart
index fe234b7a..e9b6ee00 100644
--- a/lib/models/note/note.g.dart
+++ b/lib/models/note/note.g.dart
@@ -37,18 +37,13 @@ const NoteSchema = CollectionSchema(
name: r'editedTime',
type: IsarType.dateTime,
),
- r'id': PropertySchema(
- id: 4,
- name: r'id',
- type: IsarType.string,
- ),
r'pinned': PropertySchema(
- id: 5,
+ id: 4,
name: r'pinned',
type: IsarType.bool,
),
r'title': PropertySchema(
- id: 6,
+ id: 5,
name: r'title',
type: IsarType.string,
)
@@ -57,7 +52,7 @@ const NoteSchema = CollectionSchema(
serialize: _noteSerialize,
deserialize: _noteDeserialize,
deserializeProp: _noteDeserializeProp,
- idName: r'isarId',
+ idName: r'id',
indexes: {
r'deleted': IndexSchema(
id: 2416515181749931262,
@@ -91,7 +86,7 @@ const NoteSchema = CollectionSchema(
getId: _noteGetId,
getLinks: _noteGetLinks,
attach: _noteAttach,
- version: '3.1.0+1',
+ version: '3.1.7',
);
int _noteEstimateSize(
@@ -101,12 +96,6 @@ int _noteEstimateSize(
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.content.length * 3;
- {
- final value = object.id;
- if (value != null) {
- bytesCount += 3 + value.length * 3;
- }
- }
bytesCount += 3 + object.title.length * 3;
return bytesCount;
}
@@ -121,9 +110,8 @@ void _noteSerialize(
writer.writeDateTime(offsets[1], object.createdTime);
writer.writeBool(offsets[2], object.deleted);
writer.writeDateTime(offsets[3], object.editedTime);
- writer.writeString(offsets[4], object.id);
- writer.writeBool(offsets[5], object.pinned);
- writer.writeString(offsets[6], object.title);
+ writer.writeBool(offsets[4], object.pinned);
+ writer.writeString(offsets[5], object.title);
}
Note _noteDeserialize(
@@ -137,10 +125,10 @@ Note _noteDeserialize(
createdTime: reader.readDateTime(offsets[1]),
deleted: reader.readBool(offsets[2]),
editedTime: reader.readDateTime(offsets[3]),
- id: reader.readStringOrNull(offsets[4]),
- pinned: reader.readBool(offsets[5]),
- title: reader.readString(offsets[6]),
+ pinned: reader.readBool(offsets[4]),
+ title: reader.readString(offsets[5]),
);
+ object.id = id;
return object;
}
@@ -160,10 +148,8 @@ P _noteDeserializeProp(
case 3:
return (reader.readDateTime(offset)) as P;
case 4:
- return (reader.readStringOrNull(offset)) as P;
- case 5:
return (reader.readBool(offset)) as P;
- case 6:
+ case 5:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -171,17 +157,19 @@ P _noteDeserializeProp
(
}
Id _noteGetId(Note object) {
- return object.isarId;
+ return object.id;
}
List> _noteGetLinks(Note object) {
return [];
}
-void _noteAttach(IsarCollection col, Id id, Note object) {}
+void _noteAttach(IsarCollection col, Id id, Note object) {
+ object.id = id;
+}
extension NoteQueryWhereSort on QueryBuilder {
- QueryBuilder anyIsarId() {
+ QueryBuilder anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
@@ -205,64 +193,64 @@ extension NoteQueryWhereSort on QueryBuilder {
}
extension NoteQueryWhere on QueryBuilder {
- QueryBuilder isarIdEqualTo(Id isarId) {
+ QueryBuilder idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
- lower: isarId,
- upper: isarId,
+ lower: id,
+ upper: id,
));
});
}
- QueryBuilder isarIdNotEqualTo(Id isarId) {
+ QueryBuilder idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
- IdWhereClause.lessThan(upper: isarId, includeUpper: false),
+ IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
- IdWhereClause.greaterThan(lower: isarId, includeLower: false),
+ IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
- IdWhereClause.greaterThan(lower: isarId, includeLower: false),
+ IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
- IdWhereClause.lessThan(upper: isarId, includeUpper: false),
+ IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
- QueryBuilder isarIdGreaterThan(Id isarId, {bool include = false}) {
+ QueryBuilder idGreaterThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
- IdWhereClause.greaterThan(lower: isarId, includeLower: include),
+ IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
- QueryBuilder isarIdLessThan(Id isarId, {bool include = false}) {
+ QueryBuilder idLessThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
- IdWhereClause.lessThan(upper: isarId, includeUpper: include),
+ IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
- QueryBuilder isarIdBetween(
- Id lowerIsarId,
- Id upperIsarId, {
+ QueryBuilder idBetween(
+ Id lowerId,
+ Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
- lower: lowerIsarId,
+ lower: lowerId,
includeLower: includeLower,
- upper: upperIsarId,
+ upper: upperId,
includeUpper: includeUpper,
));
});
@@ -595,184 +583,42 @@ extension NoteQueryFilter on QueryBuilder {
});
}
- QueryBuilder idIsNull() {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(const FilterCondition.isNull(
- property: r'id',
- ));
- });
- }
-
- QueryBuilder idIsNotNull() {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(const FilterCondition.isNotNull(
- property: r'id',
- ));
- });
- }
-
- QueryBuilder idEqualTo(
- String? value, {
- bool caseSensitive = true,
- }) {
+ QueryBuilder idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
value: value,
- caseSensitive: caseSensitive,
));
});
}
QueryBuilder idGreaterThan(
- String? value, {
+ Id value, {
bool include = false,
- bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
- caseSensitive: caseSensitive,
));
});
}
QueryBuilder idLessThan(
- String? value, {
+ Id value, {
bool include = false,
- bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
- caseSensitive: caseSensitive,
));
});
}
QueryBuilder idBetween(
- String? lower,
- String? upper, {
- bool includeLower = true,
- bool includeUpper = true,
- bool caseSensitive = true,
- }) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.between(
- property: r'id',
- lower: lower,
- includeLower: includeLower,
- upper: upper,
- includeUpper: includeUpper,
- caseSensitive: caseSensitive,
- ));
- });
- }
-
- QueryBuilder idStartsWith(
- String value, {
- bool caseSensitive = true,
- }) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.startsWith(
- property: r'id',
- value: value,
- caseSensitive: caseSensitive,
- ));
- });
- }
-
- QueryBuilder idEndsWith(
- String value, {
- bool caseSensitive = true,
- }) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.endsWith(
- property: r'id',
- value: value,
- caseSensitive: caseSensitive,
- ));
- });
- }
-
- QueryBuilder idContains(String value, {bool caseSensitive = true}) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.contains(
- property: r'id',
- value: value,
- caseSensitive: caseSensitive,
- ));
- });
- }
-
- QueryBuilder idMatches(String pattern, {bool caseSensitive = true}) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.matches(
- property: r'id',
- wildcard: pattern,
- caseSensitive: caseSensitive,
- ));
- });
- }
-
- QueryBuilder idIsEmpty() {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.equalTo(
- property: r'id',
- value: '',
- ));
- });
- }
-
- QueryBuilder idIsNotEmpty() {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.greaterThan(
- property: r'id',
- value: '',
- ));
- });
- }
-
- QueryBuilder isarIdEqualTo(Id value) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.equalTo(
- property: r'isarId',
- value: value,
- ));
- });
- }
-
- QueryBuilder isarIdGreaterThan(
- Id value, {
- bool include = false,
- }) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.greaterThan(
- include: include,
- property: r'isarId',
- value: value,
- ));
- });
- }
-
- QueryBuilder isarIdLessThan(
- Id value, {
- bool include = false,
- }) {
- return QueryBuilder.apply(this, (query) {
- return query.addFilterCondition(FilterCondition.lessThan(
- include: include,
- property: r'isarId',
- value: value,
- ));
- });
- }
-
- QueryBuilder isarIdBetween(
Id lower,
Id upper, {
bool includeLower = true,
@@ -780,7 +626,7 @@ extension NoteQueryFilter on QueryBuilder {
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
- property: r'isarId',
+ property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
@@ -978,18 +824,6 @@ extension NoteQuerySortBy on QueryBuilder {
});
}
- QueryBuilder sortById() {
- return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'id', Sort.asc);
- });
- }
-
- QueryBuilder sortByIdDesc() {
- return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'id', Sort.desc);
- });
- }
-
QueryBuilder sortByPinned() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'pinned', Sort.asc);
@@ -1076,18 +910,6 @@ extension NoteQuerySortThenBy on QueryBuilder {
});
}
- QueryBuilder thenByIsarId() {
- return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'isarId', Sort.asc);
- });
- }
-
- QueryBuilder thenByIsarIdDesc() {
- return QueryBuilder.apply(this, (query) {
- return query.addSortBy(r'isarId', Sort.desc);
- });
- }
-
QueryBuilder thenByPinned() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'pinned', Sort.asc);
@@ -1138,12 +960,6 @@ extension NoteQueryWhereDistinct on QueryBuilder {
});
}
- QueryBuilder distinctById({bool caseSensitive = true}) {
- return QueryBuilder.apply(this, (query) {
- return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
- });
- }
-
QueryBuilder distinctByPinned() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'pinned');
@@ -1158,9 +974,9 @@ extension NoteQueryWhereDistinct on QueryBuilder {
}
extension NoteQueryProperty on QueryBuilder {
- QueryBuilder isarIdProperty() {
+ QueryBuilder idProperty() {
return QueryBuilder.apply(this, (query) {
- return query.addPropertyName(r'isarId');
+ return query.addPropertyName(r'id');
});
}
@@ -1188,12 +1004,6 @@ extension NoteQueryProperty on QueryBuilder {
});
}
- QueryBuilder idProperty() {
- return QueryBuilder.apply(this, (query) {
- return query.addPropertyName(r'id');
- });
- }
-
QueryBuilder pinnedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'pinned');
diff --git a/lib/pages/bin/bin_page.dart b/lib/pages/bin/bin_page.dart
index 7e859e43..daabd3c3 100644
--- a/lib/pages/bin/bin_page.dart
+++ b/lib/pages/bin/bin_page.dart
@@ -1,17 +1,9 @@
+import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
-import 'package:localmaterialnotes/common/placeholders/empty_placeholder.dart';
-import 'package:localmaterialnotes/common/placeholders/error_placeholder.dart';
-import 'package:localmaterialnotes/common/placeholders/loading_placeholder.dart';
-import 'package:localmaterialnotes/common/widgets/note_tile.dart';
-import 'package:localmaterialnotes/providers/bin/bin_provider.dart';
-import 'package:localmaterialnotes/providers/layout/layout_provider.dart';
-import 'package:localmaterialnotes/utils/constants/paddings.dart';
-import 'package:localmaterialnotes/utils/constants/separators.dart';
-import 'package:localmaterialnotes/utils/constants/sizes.dart';
-import 'package:localmaterialnotes/utils/preferences/layout.dart';
-import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/common/actions/select.dart';
+import 'package:localmaterialnotes/common/widgets/notes_list.dart';
+import 'package:localmaterialnotes/providers/notifiers.dart';
class BinPage extends ConsumerStatefulWidget {
const BinPage();
@@ -22,54 +14,31 @@ class BinPage extends ConsumerStatefulWidget {
class _BinPageState extends ConsumerState {
@override
- Widget build(BuildContext context) {
- return ref.watch(binProvider).when(
- data: (notes) {
- if (notes.isEmpty) {
- return EmptyPlaceholder.bin();
- }
+ void initState() {
+ super.initState();
+
+ BackButtonInterceptor.add(_interceptor);
+ }
- final layout = ref.watch(layoutStateProvider) ?? Layout.fromPreference();
- final useSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault();
- final showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault();
+ @override
+ void dispose() {
+ BackButtonInterceptor.remove(_interceptor);
+
+ super.dispose();
+ }
- // Use at least 2 columns for the grid view
- final columnsCount = MediaQuery.of(context).size.width ~/ Sizes.custom.gridLayoutColumnWidth;
- final crossAxisCount = columnsCount > 2 ? columnsCount : 2;
+ bool _interceptor(bool stopDefaultButtonEvent, RouteInfo info) {
+ if (!isSelectionModeNotifier.value) {
+ return false;
+ }
- return layout == Layout.list
- ? ListView.separated(
- padding: showTilesBackground ? Paddings.custom.notesWithBackground : Paddings.custom.fab,
- itemCount: notes.length,
- itemBuilder: (context, index) {
- return NoteTile(notes[index]);
- },
- separatorBuilder: (BuildContext context, int index) {
- return Padding(
- padding: showTilesBackground
- ? Paddings.custom.notesListViewWithBackgroundSeparation
- : EdgeInsetsDirectional.zero,
- child: useSeparators ? Separator.divider1indent8.horizontal : null,
- );
- },
- )
- : AlignedGridView.count(
- padding: Paddings.custom.notesWithBackground,
- mainAxisSpacing: Sizes.custom.notesGridViewSpacing,
- crossAxisSpacing: Sizes.custom.notesGridViewSpacing,
- crossAxisCount: crossAxisCount,
- itemCount: notes.length,
- itemBuilder: (context, index) {
- return NoteTile(notes[index]);
- },
- );
- },
- error: (error, stackTrace) {
- return const ErrorPlaceholder();
- },
- loading: () {
- return const LoadingPlaceholder();
- },
- );
+ exitSelectionMode(ref);
+
+ return true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return const NotesList.bin();
}
}
diff --git a/lib/pages/editor/editor_page.dart b/lib/pages/editor/editor_page.dart
index aa3eef75..a3d0ced7 100644
--- a/lib/pages/editor/editor_page.dart
+++ b/lib/pages/editor/editor_page.dart
@@ -8,11 +8,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localmaterialnotes/common/placeholders/loading_placeholder.dart';
import 'package:localmaterialnotes/common/routing/router.dart';
import 'package:localmaterialnotes/models/note/note.dart';
-import 'package:localmaterialnotes/pages/editor/editor_field.dart';
-import 'package:localmaterialnotes/pages/editor/editor_toolbar.dart';
-import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart';
-import 'package:localmaterialnotes/providers/editor_controller/editor_controller_provider.dart';
+import 'package:localmaterialnotes/pages/editor/widgets/editor_field.dart';
+import 'package:localmaterialnotes/pages/editor/widgets/editor_toolbar.dart';
import 'package:localmaterialnotes/providers/notes/notes_provider.dart';
+import 'package:localmaterialnotes/providers/notifiers.dart';
import 'package:localmaterialnotes/utils/constants/constants.dart';
import 'package:localmaterialnotes/utils/constants/paddings.dart';
import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
@@ -31,9 +30,6 @@ class EditorPage extends ConsumerStatefulWidget {
class _EditorState extends ConsumerState {
final titleController = TextEditingController();
- FleatherController? fleatherController;
-
- bool fleatherFieldHasFocus = false;
@override
void initState() {
@@ -70,35 +66,35 @@ class _EditorState extends ConsumerState {
}
void _synchronizeContent(Note note) {
- if (fleatherController == null) {
+ final editorController = fleatherControllerNotifier.value;
+
+ if (editorController == null) {
return;
}
- note.content = jsonEncode(fleatherController!.document.toDelta().toJson());
+ fleatherControllerCanUndoNotifier.value = editorController.canUndo;
+ fleatherControllerCanRedoNotifier.value = editorController.canRedo;
+
+ note.content = jsonEncode(editorController.document.toDelta().toJson());
ref.read(notesProvider.notifier).edit(note);
}
@override
Widget build(BuildContext context) {
- final note = ref.watch(currentNoteProvider);
+ final note = currentNoteNotifier.value;
if (note == null) {
return const LoadingPlaceholder();
}
- final showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault();
+ final editorController = fleatherControllerNotifier.value ??= FleatherController(
+ document: note.document,
+ )..addListener(() => _synchronizeContent(note));
titleController.text = note.title;
- if (fleatherController == null) {
- fleatherController = FleatherController(document: note.document);
- fleatherController!.addListener(() => _synchronizeContent(note));
-
- Future(() {
- ref.read(editorControllerProvider.notifier).set(fleatherController!);
- });
- }
+ final showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault();
return Column(
children: [
@@ -123,7 +119,7 @@ class _EditorState extends ConsumerState {
child: Focus(
onFocusChange: (hasFocus) => fleatherFieldHasFocusNotifier.value = hasFocus,
child: EditorField(
- fleatherController: fleatherController!,
+ fleatherController: editorController,
readOnly: widget._readOnly,
autofocus: widget._autofocus,
),
@@ -135,11 +131,11 @@ class _EditorState extends ConsumerState {
),
ValueListenableBuilder(
valueListenable: fleatherFieldHasFocusNotifier,
- builder: (_, hasFocus, ___) {
+ builder: (context, hasFocus, child) {
return showToolbar && hasFocus && KeyboardVisibilityProvider.isKeyboardVisible(context)
? ColoredBox(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
- child: EditorToolbar(fleatherController!),
+ child: EditorToolbar(editorController),
)
: Container();
},
diff --git a/lib/pages/editor/editor_toolbar.dart b/lib/pages/editor/editor_toolbar.dart
deleted file mode 100644
index 812a96ea..00000000
--- a/lib/pages/editor/editor_toolbar.dart
+++ /dev/null
@@ -1,133 +0,0 @@
-import 'package:fleather/fleather.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:localmaterialnotes/utils/constants/paddings.dart';
-import 'package:localmaterialnotes/utils/constants/sizes.dart';
-import 'package:material_symbols_icons/material_symbols_icons.dart';
-
-class EditorToolbar extends ConsumerStatefulWidget {
- const EditorToolbar(this.fleatherController);
-
- final FleatherController fleatherController;
-
- @override
- ConsumerState createState() => _EditorToolbarState();
-}
-
-class _EditorToolbarState extends ConsumerState {
- Widget _buttonBuilder(
- BuildContext context,
- ParchmentAttribute attribute,
- IconData icon,
- bool isToggled,
- VoidCallback? onPressed,
- ) {
- return isToggled
- ? IconButton.filled(
- visualDensity: VisualDensity.compact,
- icon: Icon(icon),
- onPressed: onPressed,
- )
- : IconButton(
- visualDensity: VisualDensity.compact,
- icon: Icon(icon),
- onPressed: onPressed,
- );
- }
-
- void _insertRule() {
- final controller = widget.fleatherController;
-
- final index = controller.selection.baseOffset;
- final length = controller.selection.extentOffset - index;
- final newSelection = controller.selection.copyWith(
- baseOffset: index + 2,
- extentOffset: index + 2,
- );
-
- controller.replaceText(index, length, BlockEmbed.horizontalRule, selection: newSelection);
- }
-
- @override
- Widget build(BuildContext context) {
- final buttons = [
- ToggleStyleButton(
- attribute: ParchmentAttribute.bold,
- icon: Icons.format_bold,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- ToggleStyleButton(
- attribute: ParchmentAttribute.italic,
- icon: Icons.format_italic,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- ToggleStyleButton(
- attribute: ParchmentAttribute.strikethrough,
- icon: Icons.format_strikethrough,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- ToggleStyleButton(
- attribute: ParchmentAttribute.block.bulletList,
- icon: Icons.format_list_bulleted,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- ToggleStyleButton(
- attribute: ParchmentAttribute.block.numberList,
- icon: Icons.format_list_numbered,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- ToggleStyleButton(
- attribute: ParchmentAttribute.inlineCode,
- icon: Icons.code,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- ToggleStyleButton(
- attribute: ParchmentAttribute.block.code,
- icon: Symbols.code_blocks,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- ToggleStyleButton(
- attribute: ParchmentAttribute.block.quote,
- icon: Icons.format_quote,
- controller: widget.fleatherController,
- childBuilder: _buttonBuilder,
- ),
- /* TODO Add the link button
- * Right now it requires to copy too much code
- * (cf. https://github.com/fleather-editor/fleather/issues/353)
- IconButton(
- visualDensity: VisualDensity.compact,
- icon: const Icon(Icons.link),
- onPressed: () => {},
- ),
- */
- IconButton(
- visualDensity: VisualDensity.compact,
- icon: const Icon(Icons.horizontal_rule),
- onPressed: _insertRule,
- ),
- ];
-
- return SizedBox(
- height: Sizes.custom.editorToolbarHeight,
- child: ListView.separated(
- scrollDirection: Axis.horizontal,
- padding: Paddings.padding4.vertical.add(Paddings.padding4.horizontal),
- itemCount: buttons.length,
- itemBuilder: (BuildContext context, int index) {
- return buttons[index];
- },
- separatorBuilder: (context, index) {
- return Padding(padding: Paddings.padding2.horizontal);
- },
- ),
- );
- }
-}
diff --git a/lib/pages/editor/about_sheet.dart b/lib/pages/editor/sheets/about_sheet.dart
similarity index 89%
rename from lib/pages/editor/about_sheet.dart
rename to lib/pages/editor/sheets/about_sheet.dart
index 1d6b749c..957c9450 100644
--- a/lib/pages/editor/about_sheet.dart
+++ b/lib/pages/editor/sheets/about_sheet.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:localmaterialnotes/common/placeholders/error_placeholder.dart';
-import 'package:localmaterialnotes/providers/current_note/current_note_provider.dart';
+import 'package:localmaterialnotes/providers/notifiers.dart';
import 'package:localmaterialnotes/utils/constants/constants.dart';
import 'package:localmaterialnotes/utils/extensions/date_time_extensions.dart';
@@ -10,7 +10,7 @@ class AboutSheet extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
- final note = ref.watch(currentNoteProvider);
+ final note = currentNoteNotifier.value;
if (note == null) {
return const ErrorPlaceholder();
diff --git a/lib/pages/editor/editor_field.dart b/lib/pages/editor/widgets/editor_field.dart
similarity index 63%
rename from lib/pages/editor/editor_field.dart
rename to lib/pages/editor/widgets/editor_field.dart
index 1740def4..4d824445 100644
--- a/lib/pages/editor/editor_field.dart
+++ b/lib/pages/editor/widgets/editor_field.dart
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:localmaterialnotes/utils/constants/constants.dart';
import 'package:localmaterialnotes/utils/constants/paddings.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
import 'package:url_launcher/url_launcher_string.dart';
class EditorField extends StatelessWidget {
@@ -27,7 +28,7 @@ class EditorField extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return FleatherField(
+ final fleatherField = FleatherField(
controller: fleatherController,
autofocus: autofocus,
readOnly: readOnly,
@@ -41,5 +42,22 @@ class EditorField extends StatelessWidget {
),
padding: Paddings.custom.bottomSystemUi,
);
+
+ // If paragraph spacing should be used, return the editor directly without modifying its theme
+ if (PreferenceKey.useParagraphsSpacing.getPreferenceOrDefault()) {
+ return fleatherField;
+ }
+
+ final fleatherThemeFallback = FleatherThemeData.fallback(context);
+
+ return FleatherTheme(
+ data: fleatherThemeFallback.copyWith(
+ paragraph: TextBlockTheme(
+ style: fleatherThemeFallback.paragraph.style,
+ spacing: const VerticalSpacing.zero(),
+ ),
+ ),
+ child: fleatherField,
+ );
}
}
diff --git a/lib/pages/editor/widgets/editor_toolbar.dart b/lib/pages/editor/widgets/editor_toolbar.dart
new file mode 100644
index 00000000..cb7cef7c
--- /dev/null
+++ b/lib/pages/editor/widgets/editor_toolbar.dart
@@ -0,0 +1,132 @@
+import 'package:fleather/fleather.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:localmaterialnotes/utils/constants/paddings.dart';
+import 'package:localmaterialnotes/utils/constants/sizes.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:material_symbols_icons/material_symbols_icons.dart';
+
+class EditorToolbar extends ConsumerStatefulWidget {
+ const EditorToolbar(this.fleatherController);
+
+ final FleatherController fleatherController;
+
+ @override
+ ConsumerState createState() => _EditorToolbarState();
+}
+
+class _EditorToolbarState extends ConsumerState {
+ Widget _buttonBuilder(
+ BuildContext context,
+ ParchmentAttribute attribute,
+ IconData icon,
+ bool isToggled,
+ VoidCallback? onPressed,
+ ) {
+ return Padding(
+ padding: Paddings.padding2.horizontal,
+ child: ConstrainedBox(
+ constraints: BoxConstraints.tightFor(
+ width: Sizes.custom.editorToolbarButtonHeight,
+ height: Sizes.custom.editorToolbarButtonWidth,
+ ),
+ child: RawMaterialButton(
+ shape: const CircleBorder(),
+ visualDensity: VisualDensity.compact,
+ fillColor: isToggled ? Theme.of(context).colorScheme.secondary : null,
+ elevation: 0,
+ onPressed: onPressed,
+ child: Icon(icon),
+ ),
+ ),
+ );
+ }
+
+ void _insertRule() {
+ final index = widget.fleatherController.selection.baseOffset;
+ final length = widget.fleatherController.selection.extentOffset - index;
+ final newSelection = widget.fleatherController.selection.copyWith(
+ baseOffset: index + 2,
+ extentOffset: index + 2,
+ );
+
+ widget.fleatherController.replaceText(index, length, BlockEmbed.horizontalRule, selection: newSelection);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return FleatherToolbar(
+ padding: EdgeInsets.zero,
+ children: [
+ Padding(padding: Paddings.padding2.horizontal),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.bold,
+ icon: Icons.format_bold,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.italic,
+ icon: Icons.format_italic,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.underline,
+ icon: Icons.format_underline,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.strikethrough,
+ icon: Icons.format_strikethrough,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ if (!PreferenceKey.showChecklistButton.getPreferenceOrDefault())
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.block.checkList,
+ icon: Icons.checklist,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.block.bulletList,
+ icon: Icons.format_list_bulleted,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.block.numberList,
+ icon: Icons.format_list_numbered,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.inlineCode,
+ icon: Icons.code,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.block.code,
+ icon: Symbols.code_blocks,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ ToggleStyleButton(
+ attribute: ParchmentAttribute.block.quote,
+ icon: Icons.format_quote,
+ controller: widget.fleatherController,
+ childBuilder: _buttonBuilder,
+ ),
+ IconButton(
+ visualDensity: VisualDensity.compact,
+ icon: const Icon(Icons.horizontal_rule),
+ onPressed: _insertRule,
+ ),
+ Padding(padding: Paddings.padding2.horizontal),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/notes/notes_page.dart b/lib/pages/notes/notes_page.dart
index 42c98162..c64d814f 100644
--- a/lib/pages/notes/notes_page.dart
+++ b/lib/pages/notes/notes_page.dart
@@ -1,17 +1,9 @@
+import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
-import 'package:localmaterialnotes/common/placeholders/empty_placeholder.dart';
-import 'package:localmaterialnotes/common/placeholders/error_placeholder.dart';
-import 'package:localmaterialnotes/common/placeholders/loading_placeholder.dart';
-import 'package:localmaterialnotes/common/widgets/note_tile.dart';
-import 'package:localmaterialnotes/providers/layout/layout_provider.dart';
-import 'package:localmaterialnotes/providers/notes/notes_provider.dart';
-import 'package:localmaterialnotes/utils/constants/paddings.dart';
-import 'package:localmaterialnotes/utils/constants/separators.dart';
-import 'package:localmaterialnotes/utils/constants/sizes.dart';
-import 'package:localmaterialnotes/utils/preferences/layout.dart';
-import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/common/actions/select.dart';
+import 'package:localmaterialnotes/common/widgets/notes_list.dart';
+import 'package:localmaterialnotes/providers/notifiers.dart';
class NotesPage extends ConsumerStatefulWidget {
const NotesPage();
@@ -22,54 +14,31 @@ class NotesPage extends ConsumerStatefulWidget {
class _NotesPageState extends ConsumerState {
@override
- Widget build(BuildContext context) {
- return ref.watch(notesProvider).when(
- data: (notes) {
- if (notes.isEmpty) {
- return EmptyPlaceholder.notes();
- }
+ void initState() {
+ super.initState();
+
+ BackButtonInterceptor.add(_interceptor);
+ }
- final layout = ref.watch(layoutStateProvider) ?? Layout.fromPreference();
- final useSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault();
- final showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault();
+ @override
+ void dispose() {
+ BackButtonInterceptor.remove(_interceptor);
+
+ super.dispose();
+ }
- // Use at least 2 columns for the grid view
- final columnsCount = MediaQuery.of(context).size.width ~/ Sizes.custom.gridLayoutColumnWidth;
- final crossAxisCount = columnsCount > 2 ? columnsCount : 2;
+ bool _interceptor(bool stopDefaultButtonEvent, RouteInfo info) {
+ if (!isSelectionModeNotifier.value) {
+ return false;
+ }
- return layout == Layout.list
- ? ListView.separated(
- padding: showTilesBackground ? Paddings.custom.notesWithBackground : Paddings.custom.fab,
- itemCount: notes.length,
- itemBuilder: (context, index) {
- return NoteTile(notes[index]);
- },
- separatorBuilder: (BuildContext context, int index) {
- return Padding(
- padding: showTilesBackground
- ? Paddings.custom.notesListViewWithBackgroundSeparation
- : EdgeInsetsDirectional.zero,
- child: useSeparators ? Separator.divider1indent8.horizontal : null,
- );
- },
- )
- : AlignedGridView.count(
- padding: Paddings.custom.notesWithBackground,
- mainAxisSpacing: Sizes.custom.notesGridViewSpacing,
- crossAxisSpacing: Sizes.custom.notesGridViewSpacing,
- crossAxisCount: crossAxisCount,
- itemCount: notes.length,
- itemBuilder: (context, index) {
- return NoteTile(notes[index]);
- },
- );
- },
- error: (error, stackTrace) {
- return const ErrorPlaceholder();
- },
- loading: () {
- return const LoadingPlaceholder();
- },
- );
+ exitSelectionMode(ref);
+
+ return true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return const NotesList.notes();
}
}
diff --git a/lib/pages/settings/dialogs/auto_export_dialog.dart b/lib/pages/settings/dialogs/auto_export_dialog.dart
new file mode 100644
index 00000000..48635119
--- /dev/null
+++ b/lib/pages/settings/dialogs/auto_export_dialog.dart
@@ -0,0 +1,117 @@
+import 'package:flutter/material.dart';
+import 'package:localmaterialnotes/common/widgets/encrypt_password_form.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/constants/paddings.dart';
+import 'package:localmaterialnotes/utils/extensions/string_extension.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+
+class AutoExportDialog extends StatefulWidget {
+ const AutoExportDialog({super.key});
+
+ @override
+ State createState() => _AutoExportDialogState();
+}
+
+class _AutoExportDialogState extends State {
+ bool _encrypt = PreferenceKey.autoExportEncryption.getPreferenceOrDefault();
+ String? _password;
+
+ late bool ok;
+ late int _frequencyIndex;
+
+ final List _frequencyValues = [0.0, 1.0, 3.0, 7.0, 14.0, 30.0];
+
+ @override
+ void initState() {
+ super.initState();
+
+ _frequencyIndex = _frequencyValues.indexOf(PreferenceKey.autoExportFrequency.getPreferenceOrDefault());
+ if (_frequencyIndex == -1) {
+ // Make sure that the index is not set to -1 in case the frequency isn't in the allowed values
+ _frequencyIndex = 0;
+ }
+
+ _updateOk();
+ }
+
+ double get _frequencyValue {
+ return _frequencyValues[_frequencyIndex];
+ }
+
+ void _updateOk() {
+ ok = _frequencyValue == 0.0 || !_encrypt || (_encrypt && (_password?.isStrongPassword ?? false));
+ }
+
+ void _onFrequencyChanged(double value) {
+ setState(() {
+ _frequencyIndex = value.toInt();
+ _updateOk();
+ });
+ }
+
+ void _onChanged(bool encrypt, String? password) {
+ setState(() {
+ _encrypt = encrypt;
+ _password = password;
+ _updateOk();
+ });
+ }
+
+ void _pop({bool cancel = false}) {
+ if (cancel) {
+ Navigator.pop(context);
+
+ return;
+ }
+
+ Navigator.pop(context, (_frequencyValue, _encrypt, _password));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog.adaptive(
+ title: Text(localizations.settings_auto_export),
+ content: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ _frequencyValue == 0.0
+ ? localizations.settings_auto_export_dialog_description_disabled
+ : localizations.settings_auto_export_dialog_description_enabled(_frequencyValue.toInt().toString()),
+ ),
+ Padding(padding: Paddings.padding8.vertical),
+ Slider(
+ value: _frequencyIndex.toDouble(),
+ max: _frequencyValues.length - 1,
+ divisions: _frequencyValues.length - 1,
+ label: _frequencyValue == 0.0
+ ? localizations.settings_auto_export_disabled
+ : localizations.settings_auto_export_dialog_slider_label(_frequencyValue.toInt().toString()),
+ onChanged: _onFrequencyChanged,
+ ),
+ if (_frequencyValue != 0.0) ...[
+ Padding(padding: Paddings.padding8.vertical),
+ EncryptionPasswordForm(
+ secondaryDescription: localizations.dialog_export_encryption_secondary_description_auto,
+ onChanged: _onChanged,
+ onEditingComplete: _pop,
+ ),
+ ],
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => _pop(cancel: true),
+ child: Text(localizations.button_cancel),
+ ),
+ TextButton(
+ onPressed: ok ? _pop : null,
+ child: Text(localizations.button_ok),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/dialogs/import_dialog.dart b/lib/pages/settings/dialogs/import_dialog.dart
new file mode 100644
index 00000000..8b708913
--- /dev/null
+++ b/lib/pages/settings/dialogs/import_dialog.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:localmaterialnotes/common/widgets/password_field.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/extensions/string_extension.dart';
+
+class ImportDialog extends StatefulWidget {
+ const ImportDialog({
+ super.key,
+ required this.title,
+ });
+
+ final String title;
+
+ @override
+ State createState() => _ImportDialogState();
+}
+
+class _ImportDialogState extends State {
+ String? _password;
+
+ late bool ok;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _updateOk();
+ }
+
+ void _updateOk() {
+ ok = _password?.isStrongPassword ?? false;
+ }
+
+ void _onChanged(String? password) {
+ setState(() {
+ _password = password;
+ _updateOk();
+ });
+ }
+
+ void _pop({bool cancel = false}) {
+ if (cancel) {
+ Navigator.pop(context);
+
+ return;
+ }
+
+ Navigator.pop(context, _password);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog.adaptive(
+ title: Text(widget.title),
+ content: SingleChildScrollView(
+ child: PasswordField(
+ description: localizations.dialog_import_encryption_password_description,
+ secondaryDescription: localizations.dialog_export_encryption_description,
+ onChanged: _onChanged,
+ onEditingComplete: _pop,
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => _pop(cancel: true),
+ child: Text(localizations.button_cancel),
+ ),
+ TextButton(
+ onPressed: ok ? _pop : null,
+ child: Text(localizations.button_ok),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/dialogs/manual_export_dialog.dart b/lib/pages/settings/dialogs/manual_export_dialog.dart
new file mode 100644
index 00000000..84023eb2
--- /dev/null
+++ b/lib/pages/settings/dialogs/manual_export_dialog.dart
@@ -0,0 +1,74 @@
+import 'package:flutter/material.dart';
+import 'package:localmaterialnotes/common/widgets/encrypt_password_form.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/extensions/string_extension.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+
+class ManualExportDialog extends StatefulWidget {
+ const ManualExportDialog({
+ super.key,
+ });
+
+ @override
+ State createState() => _ManualExportDialogState();
+}
+
+class _ManualExportDialogState extends State {
+ bool _encrypt = PreferenceKey.autoExportEncryption.getPreferenceOrDefault();
+ String? _password;
+
+ late bool ok;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _updateOk();
+ }
+
+ void _updateOk() {
+ ok = !_encrypt || (_encrypt && (_password?.isStrongPassword ?? false));
+ }
+
+ void _onChanged(bool encrypt, String? password) {
+ setState(() {
+ _encrypt = encrypt;
+ _password = password;
+ _updateOk();
+ });
+ }
+
+ void _pop({bool cancel = false}) {
+ if (cancel) {
+ Navigator.pop(context);
+
+ return;
+ }
+
+ Navigator.pop(context, (_encrypt, _password));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog.adaptive(
+ title: Text(localizations.settings_export_json),
+ content: SingleChildScrollView(
+ child: EncryptionPasswordForm(
+ secondaryDescription: localizations.dialog_export_encryption_secondary_description_manual,
+ onChanged: _onChanged,
+ onEditingComplete: _pop,
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => _pop(cancel: true),
+ child: Text(localizations.button_cancel),
+ ),
+ TextButton(
+ onPressed: ok ? _pop : null,
+ child: Text(localizations.button_ok),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/enums/settings_page.dart b/lib/pages/settings/enums/settings_page.dart
new file mode 100644
index 00000000..ebc764f2
--- /dev/null
+++ b/lib/pages/settings/enums/settings_page.dart
@@ -0,0 +1,7 @@
+enum SettingsPage {
+ appearance,
+ behavior,
+ editor,
+ backup,
+ about,
+}
diff --git a/lib/pages/settings/pages/settings_about_page.dart b/lib/pages/settings/pages/settings_about_page.dart
new file mode 100644
index 00000000..dff097a3
--- /dev/null
+++ b/lib/pages/settings/pages/settings_about_page.dart
@@ -0,0 +1,176 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_settings_ui/flutter_settings_ui.dart';
+import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart';
+import 'package:localmaterialnotes/utils/asset.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/constants/paddings.dart';
+import 'package:localmaterialnotes/utils/constants/sizes.dart';
+import 'package:localmaterialnotes/utils/info_utils.dart';
+import 'package:simple_icons/simple_icons.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+/// Settings providing information about the application.
+class SettingsAboutPage extends StatelessWidget {
+ const SettingsAboutPage({super.key});
+
+ String? _encodeQueryParameters(Map params) {
+ return params.entries.map((MapEntry e) {
+ return '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}';
+ }).join('&');
+ }
+
+ /// Shows the about dialog.
+ Future _showAbout(BuildContext context) async {
+ showAboutDialog(
+ context: context,
+ useRootNavigator: false,
+ applicationName: localizations.app_name,
+ applicationVersion: InfoUtils().appVersion,
+ applicationIcon: Image.asset(
+ Asset.icon.path,
+ fit: BoxFit.fitWidth,
+ width: Sizes.size64.size,
+ ),
+ applicationLegalese: localizations.settings_licence_description,
+ children: [
+ Padding(padding: Paddings.padding16.vertical),
+ Text(
+ localizations.app_tagline,
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ Padding(padding: Paddings.padding8.vertical),
+ Text(localizations.app_about(localizations.app_name)),
+ ],
+ );
+ }
+
+ /// Opens the application's GitHub issues.
+ void _openGitHubIssues(_) {
+ launchUrl(
+ Uri(
+ scheme: 'https',
+ host: 'github.com',
+ path: 'maelchiotti/LocalMaterialNotes/issues',
+ ),
+ );
+ }
+
+ /// Opens the application's GitHub discussions.
+ void _openGitHubDiscussions(_) {
+ launchUrl(
+ Uri(
+ scheme: 'https',
+ host: 'github.com',
+ path: 'maelchiotti/LocalMaterialNotes/discussions',
+ ),
+ );
+ }
+
+ /// Sends an email to `contact@maelchiotti.dev` with some basic information.
+ void _sendMail(_) {
+ final appVersion = InfoUtils().appVersion;
+ final buildMode = InfoUtils().buildMode;
+ final androidVersion = InfoUtils().androidVersion;
+ final brand = InfoUtils().brand;
+ final model = InfoUtils().model;
+
+ launchUrl(
+ Uri(
+ scheme: 'mailto',
+ path: 'contact@maelchiotti.dev',
+ query: _encodeQueryParameters({
+ 'subject': '[Material Notes] ',
+ 'body': '\n\n\n----------\nv$appVersion\n$buildMode mode\nAndroid $androidVersion\n$brand $model',
+ }),
+ ),
+ );
+ }
+
+ /// Opens the application's GitHub repository.
+ void _openGitHub(_) {
+ launchUrl(
+ Uri(
+ scheme: 'https',
+ host: 'github.com',
+ path: 'maelchiotti/LocalMaterialNotes',
+ ),
+ );
+ }
+
+ /// Opens the application's license file.
+ void _openLicense(_) {
+ launchUrl(
+ Uri(
+ scheme: 'https',
+ host: 'github.com',
+ path: 'maelchiotti/LocalMaterialNotes/blob/main/LICENSE',
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final appVersion = InfoUtils().appVersion;
+
+ return CustomSettingsList(
+ sections: [
+ SettingsSection(
+ title: Text(localizations.settings_about_application),
+ tiles: [
+ SettingsTile(
+ leading: const Icon(Icons.info),
+ title: Text(localizations.app_name),
+ value: Text('v$appVersion'),
+ onPressed: _showAbout,
+ ),
+ SettingsTile(
+ leading: const Icon(Icons.build),
+ title: Text(localizations.settings_build_mode),
+ value: Text(InfoUtils().buildMode),
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: Text(localizations.settings_about_help),
+ tiles: [
+ SettingsTile(
+ leading: const Icon(Icons.bug_report),
+ title: Text(localizations.settings_github_issues),
+ value: Text(localizations.settings_github_issues_description),
+ onPressed: _openGitHubIssues,
+ ),
+ SettingsTile(
+ leading: const Icon(Icons.forum),
+ title: Text(localizations.settings_github_discussions),
+ value: Text(localizations.settings_github_discussions_description),
+ onPressed: _openGitHubDiscussions,
+ ),
+ SettingsTile(
+ leading: const Icon(Icons.mail),
+ title: Text(localizations.settings_get_in_touch),
+ value: Text(localizations.settings_get_in_touch_description),
+ onPressed: _sendMail,
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: Text(localizations.settings_about_links),
+ tiles: [
+ SettingsTile(
+ leading: const Icon(SimpleIcons.github),
+ title: Text(localizations.settings_github),
+ value: Text(localizations.settings_github_description),
+ onPressed: _openGitHub,
+ ),
+ SettingsTile(
+ leading: const Icon(Icons.balance),
+ title: Text(localizations.settings_licence),
+ value: Text(localizations.settings_licence_description),
+ onPressed: _openLicense,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/pages/settings_appearance_page.dart b/lib/pages/settings/pages/settings_appearance_page.dart
new file mode 100644
index 00000000..56a0acc2
--- /dev/null
+++ b/lib/pages/settings/pages/settings_appearance_page.dart
@@ -0,0 +1,205 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_settings_ui/flutter_settings_ui.dart';
+import 'package:locale_names/locale_names.dart';
+import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.dart';
+import 'package:localmaterialnotes/l10n/localization_completion.dart';
+import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart';
+import 'package:localmaterialnotes/providers/notifiers.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/extensions/string_extension.dart';
+import 'package:localmaterialnotes/utils/locale_utils.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart';
+import 'package:localmaterialnotes/utils/theme_utils.dart';
+import 'package:restart_app/restart_app.dart';
+
+/// Settings related to the appearance of the application.
+class SettingsAppearancePage extends StatefulWidget {
+ const SettingsAppearancePage({super.key});
+
+ @override
+ State createState() => _SettingsAppearancePageState();
+}
+
+class _SettingsAppearancePageState extends State {
+ /// Asks the user to select the language of the application.
+ ///
+ /// Restarts the application if the language is changed.
+ Future _selectLanguage(BuildContext context) async {
+ await showAdaptiveDialog(
+ context: context,
+ builder: (context) {
+ return SimpleDialog(
+ clipBehavior: Clip.hardEdge,
+ title: Text(localizations.settings_language),
+ children: AppLocalizations.supportedLocales.map((locale) {
+ return RadioListTile(
+ value: locale,
+ groupValue: Localizations.localeOf(context),
+ title: Text(locale.nativeDisplayLanguage.capitalized),
+ subtitle: Text(LocalizationCompletion.getFormattedPercentage(locale)),
+ selected: Localizations.localeOf(context) == locale,
+ onChanged: (locale) => Navigator.of(context).pop(locale),
+ );
+ }).toList(),
+ );
+ },
+ ).then((locale) async {
+ if (locale == null) {
+ return;
+ }
+
+ LocaleUtils().setLocale(locale);
+
+ // The Restart package crashes the app if used in debug mode
+ if (!kDebugMode) {
+ await Restart.restartApp();
+ }
+ });
+ }
+
+ /// Asks the user to select the theme of the application.
+ Future _selectTheme(BuildContext context) async {
+ await showAdaptiveDialog(
+ context: context,
+ builder: (context) {
+ return SimpleDialog(
+ clipBehavior: Clip.hardEdge,
+ title: Text(localizations.settings_theme),
+ children: [
+ RadioListTile(
+ value: ThemeMode.system,
+ groupValue: ThemeUtils().themeMode,
+ title: Text(localizations.settings_theme_system),
+ selected: ThemeUtils().themeMode == ThemeMode.system,
+ onChanged: (themeMode) => Navigator.of(context).pop(themeMode),
+ ),
+ RadioListTile(
+ value: ThemeMode.light,
+ groupValue: ThemeUtils().themeMode,
+ title: Text(localizations.settings_theme_light),
+ selected: ThemeUtils().themeMode == ThemeMode.light,
+ onChanged: (themeMode) => Navigator.of(context).pop(themeMode),
+ ),
+ RadioListTile(
+ value: ThemeMode.dark,
+ groupValue: ThemeUtils().themeMode,
+ title: Text(localizations.settings_theme_dark),
+ selected: ThemeUtils().themeMode == ThemeMode.dark,
+ onChanged: (themeMode) => Navigator.of(context).pop(themeMode),
+ ),
+ ],
+ );
+ },
+ ).then((themeMode) {
+ if (themeMode == null) {
+ return;
+ }
+
+ ThemeUtils().setThemeMode(themeMode);
+ });
+ }
+
+ /// Toggles the dynamic theming.
+ void _toggleDynamicTheming(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.dynamicTheming.name, toggled);
+ });
+
+ dynamicThemingNotifier.value = toggled;
+ }
+
+ /// Toggles the black theming.
+ void _toggleBlackTheming(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.blackTheming.name, toggled);
+ });
+
+ blackThemingNotifier.value = toggled;
+ }
+
+ /// Toggles the setting to show background of the notes tiles.
+ void _toggleShowTilesBackground(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.showTilesBackground.name, toggled);
+ });
+
+ showTilesBackgroundNotifier.value = toggled;
+ }
+
+ /// Toggles the setting to show the separators between the notes tiles.
+ void _toggleShowSeparators(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.showSeparators.name, toggled);
+ });
+
+ showSeparatorsNotifier.value = toggled;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final locale = LocaleUtils().appLocale.nativeDisplayLanguage.capitalized;
+ final showUseBlackTheming = Theme.of(context).colorScheme.brightness == Brightness.dark;
+
+ final showSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault();
+ final showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault();
+
+ return CustomSettingsList(
+ sections: [
+ SettingsSection(
+ title: Text(localizations.settings_appearance_application),
+ tiles: [
+ SettingsTile.navigation(
+ leading: const Icon(Icons.language),
+ title: Text(localizations.settings_language),
+ value: Text(locale),
+ onPressed: _selectLanguage,
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(Icons.palette),
+ title: Text(localizations.settings_theme),
+ value: Text(ThemeUtils().themeModeName),
+ onPressed: _selectTheme,
+ ),
+ SettingsTile.switchTile(
+ enabled: ThemeUtils().isDynamicThemingAvailable,
+ leading: const Icon(Icons.bolt),
+ title: Text(localizations.settings_dynamic_theming),
+ description: Text(localizations.settings_dynamic_theming_description),
+ initialValue: ThemeUtils().useDynamicTheming,
+ onToggle: _toggleDynamicTheming,
+ ),
+ SettingsTile.switchTile(
+ enabled: showUseBlackTheming,
+ leading: const Icon(Icons.nightlight),
+ title: Text(localizations.settings_black_theming),
+ description: Text(localizations.settings_black_theming_description),
+ initialValue: ThemeUtils().useBlackTheming,
+ onToggle: _toggleBlackTheming,
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: Text(localizations.settings_appearance_notes_tiles),
+ tiles: [
+ SettingsTile.switchTile(
+ leading: const Icon(Icons.safety_divider),
+ title: Text(localizations.settings_show_separators),
+ description: Text(localizations.settings_show_separators_description),
+ initialValue: showSeparators,
+ onToggle: _toggleShowSeparators,
+ ),
+ SettingsTile.switchTile(
+ leading: const Icon(Icons.gradient),
+ title: Text(localizations.settings_show_tiles_background),
+ description: Text(localizations.settings_show_tiles_background_description),
+ initialValue: showTilesBackground,
+ onToggle: _toggleShowTilesBackground,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/pages/settings_backup_page.dart b/lib/pages/settings/pages/settings_backup_page.dart
new file mode 100644
index 00000000..79bd5d26
--- /dev/null
+++ b/lib/pages/settings/pages/settings_backup_page.dart
@@ -0,0 +1,187 @@
+import 'dart:developer';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_settings_ui/flutter_settings_ui.dart';
+import 'package:localmaterialnotes/pages/settings/dialogs/auto_export_dialog.dart';
+import 'package:localmaterialnotes/pages/settings/dialogs/manual_export_dialog.dart';
+import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart';
+import 'package:localmaterialnotes/providers/bin/bin_provider.dart';
+import 'package:localmaterialnotes/providers/notes/notes_provider.dart';
+import 'package:localmaterialnotes/utils/auto_export_utils.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/database_utils.dart';
+import 'package:localmaterialnotes/utils/extensions/uri_extension.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart';
+import 'package:localmaterialnotes/utils/snack_bar_utils.dart';
+import 'package:simple_icons/simple_icons.dart';
+
+/// Settings related to backup of the notes.
+class SettingsBackupPage extends ConsumerStatefulWidget {
+ const SettingsBackupPage({super.key});
+
+ @override
+ ConsumerState createState() => _SettingsBackupPageState();
+}
+
+class _SettingsBackupPageState extends ConsumerState {
+ /// Asks the user to configure the auto export as JSON.
+ Future autoExportAsJson(BuildContext context) async {
+ await showAdaptiveDialog<(double, bool, String?)>(
+ context: context,
+ builder: (context) => const AutoExportDialog(),
+ ).then((autoExportSettings) async {
+ if (autoExportSettings == null) {
+ return;
+ }
+
+ final frequency = autoExportSettings.$1;
+ PreferencesUtils().set(PreferenceKey.autoExportFrequency.name, frequency);
+
+ // If the auto export was disabled, just remove the encryption, last export date and password settings
+ if (frequency == 0.0) {
+ await PreferencesUtils().remove(PreferenceKey.autoExportEncryption);
+ await PreferencesUtils().remove(PreferenceKey.lastAutoExportDate);
+ await PreferencesUtils().deleteSecure(PreferenceKey.autoExportPassword);
+
+ return;
+ }
+
+ final encrypt = autoExportSettings.$2;
+ PreferencesUtils().set(PreferenceKey.autoExportEncryption.name, encrypt);
+
+ // If the encryption was enabled, set the password. If not, make sure to delete it
+ // (even though it might not have been set previously)
+ if (encrypt) {
+ final password = autoExportSettings.$3!;
+ PreferencesUtils().setSecure(PreferenceKey.autoExportPassword, password);
+ } else {
+ await PreferencesUtils().deleteSecure(PreferenceKey.autoExportPassword);
+ }
+
+ setState(() {});
+
+ // No need to await this, it can be performed in the background
+ AutoExportUtils().performAutoExportIfNeeded();
+ });
+ }
+
+ /// Asks the user to configure the immediate export as JSON.
+ Future exportAsJson(BuildContext context) async {
+ await showAdaptiveDialog<(bool, String?)?>(
+ context: context,
+ builder: (context) => const ManualExportDialog(),
+ ).then((shouldEncrypt) async {
+ if (shouldEncrypt == null) {
+ return;
+ }
+
+ final encrypt = shouldEncrypt.$1;
+
+ try {
+ final password = shouldEncrypt.$2;
+
+ if (await DatabaseUtils().manuallyExportAsJson(encrypt, password)) {
+ SnackBarUtils.info(localizations.settings_export_success).show();
+ }
+ } catch (exception, stackTrace) {
+ log(exception.toString(), stackTrace: stackTrace);
+
+ SnackBarUtils.info(exception.toString()).show();
+ }
+ });
+ }
+
+ /// Asks the user to configure the immediate export as Markdown.
+ Future exportAsMarkdown(BuildContext context) async {
+ try {
+ if (await DatabaseUtils().exportAsMarkdown()) {
+ SnackBarUtils.info(localizations.settings_export_success).show();
+ }
+ } catch (exception, stackTrace) {
+ log(exception.toString(), stackTrace: stackTrace);
+
+ SnackBarUtils.info(exception.toString()).show();
+ }
+ }
+
+ /// Asks the user to choose a JSON file to import in the application.
+ Future import(BuildContext context) async {
+ try {
+ final imported = await DatabaseUtils().import(context);
+
+ if (imported) {
+ await ref.read(notesProvider.notifier).get();
+ await ref.read(binProvider.notifier).get();
+
+ SnackBarUtils.info(localizations.settings_import_success).show();
+ }
+ } catch (exception, stackTrace) {
+ log(exception.toString(), stackTrace: stackTrace);
+
+ SnackBarUtils.info(exception.toString()).show();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final autoExportFrequency = PreferenceKey.autoExportFrequency.getPreferenceOrDefault();
+ final autoExportEncryption = PreferenceKey.autoExportEncryption.getPreferenceOrDefault();
+ final autoExportDirectory = AutoExportUtils().backupsDirectory.toDecodedString;
+
+ return CustomSettingsList(
+ sections: [
+ SettingsSection(
+ title: Text(localizations.settings_backup_export),
+ tiles: [
+ SettingsTile.navigation(
+ leading: const Icon(Icons.settings_backup_restore),
+ title: Text(localizations.settings_auto_export),
+ value: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ autoExportFrequency == 0
+ ? localizations.settings_auto_export_disabled
+ : localizations.settings_auto_export_value(
+ autoExportEncryption.toString(),
+ autoExportFrequency.toInt().toString(),
+ ),
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ Text(localizations.settings_auto_export_description),
+ Text(localizations.settings_auto_export_directory(autoExportDirectory)),
+ ],
+ ),
+ onPressed: autoExportAsJson,
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(SimpleIcons.json),
+ title: Text(localizations.settings_export_json),
+ value: Text(localizations.settings_export_json_description),
+ onPressed: exportAsJson,
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(SimpleIcons.markdown),
+ title: Text(localizations.settings_export_markdown),
+ value: Text(localizations.settings_export_markdown_description),
+ onPressed: exportAsMarkdown,
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: Text(localizations.settings_backup_import),
+ tiles: [
+ SettingsTile.navigation(
+ leading: const Icon(Icons.file_upload),
+ title: Text(localizations.settings_import),
+ value: Text(localizations.settings_import_description),
+ onPressed: import,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/pages/settings_behavior_page.dart b/lib/pages/settings/pages/settings_behavior_page.dart
new file mode 100644
index 00000000..df890c0f
--- /dev/null
+++ b/lib/pages/settings/pages/settings_behavior_page.dart
@@ -0,0 +1,183 @@
+import 'package:flag_secure/flag_secure.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_settings_ui/flutter_settings_ui.dart';
+import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart';
+import 'package:localmaterialnotes/providers/notifiers.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/preferences/enums/confirmations.dart';
+import 'package:localmaterialnotes/utils/preferences/enums/swipe_action.dart';
+import 'package:localmaterialnotes/utils/preferences/enums/swipe_direction.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart';
+
+/// Settings related to the behavior of the application.
+class SettingsBehaviorPage extends StatefulWidget {
+ const SettingsBehaviorPage({super.key});
+
+ @override
+ State createState() => _SettingsBehaviorPageState();
+}
+
+class _SettingsBehaviorPageState extends State {
+ /// Asks the user to choose which confirmations should be shown.
+ Future _selectConfirmations(BuildContext context) async {
+ final confirmationsPreference = Confirmations.fromPreference();
+
+ await showAdaptiveDialog(
+ context: context,
+ builder: (context) {
+ return SimpleDialog(
+ clipBehavior: Clip.hardEdge,
+ title: Text(localizations.settings_confirmations),
+ children: Confirmations.values.map((confirmationsValue) {
+ return RadioListTile(
+ value: confirmationsValue,
+ groupValue: confirmationsPreference,
+ title: Text(confirmationsValue.title),
+ selected: confirmationsPreference == confirmationsValue,
+ onChanged: (confirmations) => Navigator.of(context).pop(confirmations),
+ );
+ }).toList(),
+ );
+ },
+ ).then((confirmations) {
+ if (confirmations == null) {
+ return;
+ }
+
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.confirmations.name, confirmations.name);
+ });
+ });
+ }
+
+ /// Asks the user to choose which action should be triggered when swiping the notes tiles in the [swipeDirection].
+ Future _selectSwipeAction(BuildContext context, SwipeDirection swipeDirection) async {
+ SwipeAction swipeActionPreference;
+ switch (swipeDirection) {
+ case SwipeDirection.right:
+ swipeActionPreference = swipeActionsNotifier.value.$1;
+ case SwipeDirection.left:
+ swipeActionPreference = swipeActionsNotifier.value.$2;
+ }
+
+ await showAdaptiveDialog(
+ context: context,
+ builder: (context) {
+ return SimpleDialog(
+ clipBehavior: Clip.hardEdge,
+ title: Text(localizations.settings_confirmations),
+ children: SwipeAction.values.map((swipeAction) {
+ return RadioListTile(
+ value: swipeAction,
+ groupValue: swipeActionPreference,
+ title: Text(swipeAction.title),
+ selected: swipeActionPreference == swipeAction,
+ onChanged: (swipeAction) => Navigator.of(context).pop(swipeAction),
+ );
+ }).toList(),
+ );
+ },
+ ).then((swipeAction) {
+ if (swipeAction == null) {
+ return;
+ }
+
+ setState(() {
+ switch (swipeDirection) {
+ case SwipeDirection.right:
+ PreferencesUtils().set(PreferenceKey.swipeRightAction.name, swipeAction.name);
+ swipeActionsNotifier.value = (swipeAction, swipeActionsNotifier.value.$2);
+ case SwipeDirection.left:
+ PreferencesUtils().set(PreferenceKey.swipeLeftAction.name, swipeAction.name);
+ swipeActionsNotifier.value = (swipeActionsNotifier.value.$1, swipeAction);
+ }
+ });
+ });
+ }
+
+ /// Toggles Android's `FLAG_SECURE` to hide the app from the recent apps and prevent screenshots.
+ Future _setFlagSecure(bool toggled) async {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.flagSecure.name, toggled);
+ });
+
+ toggled ? await FlagSecure.set() : await FlagSecure.unset();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final confirmations = Confirmations.fromPreference();
+ final flagSecure = PreferenceKey.flagSecure.getPreferenceOrDefault();
+
+ final swipeRightAction = swipeActionsNotifier.value.$1;
+ final swipeLeftAction = swipeActionsNotifier.value.$2;
+
+ return CustomSettingsList(
+ sections: [
+ SettingsSection(
+ title: Text(localizations.settings_behavior_application),
+ tiles: [
+ SettingsTile.navigation(
+ leading: const Icon(Icons.warning),
+ title: Text(localizations.settings_confirmations),
+ value: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ confirmations.title,
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ Text(localizations.settings_confirmations_description),
+ ],
+ ),
+ onPressed: _selectConfirmations,
+ ),
+ SettingsTile.switchTile(
+ leading: const Icon(Icons.security),
+ title: Text(localizations.settings_flag_secure),
+ description: Text(localizations.settings_flag_secure_description),
+ initialValue: flagSecure,
+ onToggle: _setFlagSecure,
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: Text(localizations.settings_behavior_swipe_actions),
+ tiles: [
+ SettingsTile.navigation(
+ leading: const Icon(Icons.swipe_right),
+ title: Text(localizations.settings_swipe_action_right),
+ value: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ swipeRightAction.title,
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ Text(localizations.settings_swipe_action_right_description),
+ ],
+ ),
+ onPressed: (context) => _selectSwipeAction(context, SwipeDirection.right),
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(Icons.swipe_left),
+ title: Text(localizations.settings_swipe_action_left),
+ value: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ swipeLeftAction.title,
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ Text(localizations.settings_swipe_action_left_description),
+ ],
+ ),
+ onPressed: (context) => _selectSwipeAction(context, SwipeDirection.left),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/pages/settings_editor_page.dart b/lib/pages/settings/pages/settings_editor_page.dart
new file mode 100644
index 00000000..89497988
--- /dev/null
+++ b/lib/pages/settings/pages/settings_editor_page.dart
@@ -0,0 +1,99 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_settings_ui/flutter_settings_ui.dart';
+import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart';
+
+/// Settings related to the notes editor.
+class SettingsEditorPage extends StatefulWidget {
+ const SettingsEditorPage({super.key});
+
+ @override
+ State createState() => _SettingsEditorPageState();
+}
+
+class _SettingsEditorPageState extends State {
+ /// Toggles the setting to show the undo/redo buttons in the editor's app bar.
+ void _toggleShowUndoRedoButtons(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.showUndoRedoButtons.name, toggled);
+ });
+ }
+
+ /// Toggles the setting to show the checklist button in the editor's app bar or toolbar.
+ ///
+ /// If the editor's toolbar is enabled, the checklist button is shown inside it.
+ /// Otherwise, it's shown in the editor's app bar.
+ void _toggleShowChecklistButton(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.showChecklistButton.name, toggled);
+ });
+ }
+
+ /// Toggles the setting to show the editor's toolbar.
+ void _toggleShowToolbar(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.showToolbar.name, toggled);
+ });
+ }
+
+ /// Toggles the setting to use spacing between the paragraphs.
+ void _toggleUseParagraphSpacing(bool toggled) {
+ setState(() {
+ PreferencesUtils().set(PreferenceKey.useParagraphsSpacing.name, toggled);
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final bool showUndoRedoButtons = PreferenceKey.showUndoRedoButtons.getPreferenceOrDefault();
+ final bool showChecklistButton = PreferenceKey.showChecklistButton.getPreferenceOrDefault();
+ final bool showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault();
+
+ final bool useParagraphsSpacing = PreferenceKey.useParagraphsSpacing.getPreferenceOrDefault();
+
+ return CustomSettingsList(
+ sections: [
+ SettingsSection(
+ title: Text(localizations.settings_editor_formatting),
+ tiles: [
+ SettingsTile.switchTile(
+ leading: const Icon(Icons.undo),
+ title: Text(localizations.settings_show_undo_redo_buttons),
+ description: Text(localizations.settings_show_undo_redo_buttons_description),
+ initialValue: showUndoRedoButtons,
+ onToggle: _toggleShowUndoRedoButtons,
+ ),
+ SettingsTile.switchTile(
+ leading: const Icon(Icons.checklist),
+ title: Text(localizations.settings_show_checklist_button),
+ description: Text(localizations.settings_show_checklist_button_description),
+ initialValue: showChecklistButton,
+ onToggle: _toggleShowChecklistButton,
+ ),
+ SettingsTile.switchTile(
+ leading: const Icon(Icons.format_paint),
+ title: Text(localizations.settings_show_toolbar),
+ description: Text(localizations.settings_show_toolbar_description),
+ initialValue: showToolbar,
+ onToggle: _toggleShowToolbar,
+ ),
+ ],
+ ),
+ SettingsSection(
+ title: Text(localizations.settings_editor_appearance),
+ tiles: [
+ SettingsTile.switchTile(
+ leading: const Icon(Icons.format_line_spacing),
+ title: Text(localizations.settings_use_paragraph_spacing),
+ description: Text(localizations.settings_use_paragraph_spacing_description),
+ initialValue: useParagraphsSpacing,
+ onToggle: _toggleUseParagraphSpacing,
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/settings_actions.dart b/lib/pages/settings/settings_actions.dart
deleted file mode 100644
index a5b3d489..00000000
--- a/lib/pages/settings/settings_actions.dart
+++ /dev/null
@@ -1,238 +0,0 @@
-import 'dart:developer';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:locale_names/locale_names.dart';
-import 'package:localmaterialnotes/l10n/app_localizations/app_localizations.g.dart';
-import 'package:localmaterialnotes/providers/bin/bin_provider.dart';
-import 'package:localmaterialnotes/providers/notes/notes_provider.dart';
-import 'package:localmaterialnotes/utils/asset.dart';
-import 'package:localmaterialnotes/utils/constants/constants.dart';
-import 'package:localmaterialnotes/utils/constants/paddings.dart';
-import 'package:localmaterialnotes/utils/constants/sizes.dart';
-import 'package:localmaterialnotes/utils/database_utils.dart';
-import 'package:localmaterialnotes/utils/extensions/string_extension.dart';
-import 'package:localmaterialnotes/utils/info_utils.dart';
-import 'package:localmaterialnotes/utils/locale_utils.dart';
-import 'package:localmaterialnotes/utils/preferences/confirmations.dart';
-import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
-import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart';
-import 'package:localmaterialnotes/utils/snack_bar_utils.dart';
-import 'package:localmaterialnotes/utils/theme_utils.dart';
-import 'package:restart_app/restart_app.dart';
-import 'package:url_launcher/url_launcher.dart';
-
-class SettingsActions {
- void toggleBooleanSetting(PreferenceKey preferenceKey, bool toggled) {
- PreferencesUtils().set(preferenceKey.name, toggled);
- }
-
- Future selectLanguage(BuildContext context) async {
- await showAdaptiveDialog(
- context: context,
- builder: (context) {
- return SimpleDialog(
- clipBehavior: Clip.hardEdge,
- title: Text(localizations.settings_language),
- children: AppLocalizations.supportedLocales.map((locale) {
- return RadioListTile(
- value: locale,
- groupValue: Localizations.localeOf(context),
- title: Text(locale.nativeDisplayLanguage.capitalized),
- selected: Localizations.localeOf(context) == locale,
- onChanged: (locale) => Navigator.of(context).pop(locale),
- );
- }).toList(),
- );
- },
- ).then((locale) async {
- if (locale == null) {
- return;
- }
-
- LocaleUtils().setLocale(locale);
-
- // The Restart package crashes the app if used in debug mode
- if (!kDebugMode) {
- await Restart.restartApp();
- }
- });
- }
-
- Future selectTheme(BuildContext context) async {
- await showAdaptiveDialog(
- context: context,
- builder: (context) {
- return SimpleDialog(
- clipBehavior: Clip.hardEdge,
- title: Text(localizations.settings_theme),
- children: [
- RadioListTile(
- value: ThemeMode.system,
- groupValue: ThemeUtils().themeMode,
- title: Text(localizations.settings_theme_system),
- selected: ThemeUtils().themeMode == ThemeMode.system,
- onChanged: (locale) => Navigator.of(context).pop(locale),
- ),
- RadioListTile(
- value: ThemeMode.light,
- groupValue: ThemeUtils().themeMode,
- title: Text(localizations.settings_theme_light),
- selected: ThemeUtils().themeMode == ThemeMode.light,
- onChanged: (locale) => Navigator.of(context).pop(locale),
- ),
- RadioListTile(
- value: ThemeMode.dark,
- groupValue: ThemeUtils().themeMode,
- title: Text(localizations.settings_theme_dark),
- selected: ThemeUtils().themeMode == ThemeMode.dark,
- onChanged: (locale) => Navigator.of(context).pop(locale),
- ),
- ],
- );
- },
- ).then((themeMode) {
- if (themeMode == null) {
- return;
- }
-
- ThemeUtils().setThemeMode(themeMode);
- });
- }
-
- void toggleDynamicTheming(bool toggled) {
- PreferencesUtils().set(PreferenceKey.dynamicTheming.name, toggled);
- dynamicThemingNotifier.value = toggled;
- }
-
- void toggleBlackTheming(bool toggled) {
- PreferencesUtils().set(PreferenceKey.blackTheming.name, toggled);
- blackThemingNotifier.value = toggled;
- }
-
- Future selectConfirmations(BuildContext context) async {
- final confirmationsPreference = Confirmations.fromPreference();
-
- await showAdaptiveDialog(
- context: context,
- builder: (context) {
- return SimpleDialog(
- clipBehavior: Clip.hardEdge,
- title: Text(localizations.settings_confirmations),
- children: Confirmations.values.map((confirmationsValue) {
- return RadioListTile(
- value: confirmationsValue,
- groupValue: confirmationsPreference,
- title: Text(confirmationsValue.title),
- selected: confirmationsPreference == confirmationsValue,
- onChanged: (locale) => Navigator.of(context).pop(locale),
- );
- }).toList(),
- );
- },
- ).then((confirmationsValue) {
- if (confirmationsValue == null) {
- return;
- }
-
- PreferencesUtils().set(PreferenceKey.confirmations.name, confirmationsValue.name);
- });
- }
-
- Future backupAsJson(BuildContext context) async {
- try {
- if (await DatabaseUtils().exportAsJson()) {
- SnackBarUtils.info(localizations.settings_export_success).show();
- }
- } catch (exception, stackTrace) {
- log(exception.toString(), stackTrace: stackTrace);
-
- SnackBarUtils.info(exception.toString()).show();
- }
- }
-
- Future backupAsMarkdown(BuildContext context) async {
- try {
- if (await DatabaseUtils().exportAsMarkdown()) {
- SnackBarUtils.info(localizations.settings_export_success).show();
- }
- } catch (exception, stackTrace) {
- log(exception.toString(), stackTrace: stackTrace);
-
- SnackBarUtils.info(exception.toString()).show();
- }
- }
-
- Future import(BuildContext context, WidgetRef ref) async {
- try {
- final imported = await DatabaseUtils().import();
-
- if (imported) {
- await ref.read(notesProvider.notifier).get();
- await ref.read(binProvider.notifier).get();
-
- SnackBarUtils.info(localizations.settings_import_success).show();
- }
- } catch (exception, stackTrace) {
- log(exception.toString(), stackTrace: stackTrace);
-
- SnackBarUtils.info(exception.toString()).show();
- }
- }
-
- Future showAbout(BuildContext context) async {
- showAboutDialog(
- context: context,
- useRootNavigator: false,
- applicationName: localizations.app_name,
- applicationVersion: InfoUtils().appVersion,
- applicationIcon: Image.asset(
- Asset.icon.path,
- filterQuality: FilterQuality.medium,
- fit: BoxFit.fitWidth,
- width: Sizes.size64.size,
- ),
- applicationLegalese: localizations.settings_licence_description,
- children: [
- Padding(padding: Paddings.padding16.vertical),
- Text(
- localizations.app_tagline,
- style: Theme.of(context).textTheme.titleSmall,
- ),
- Padding(padding: Paddings.padding8.vertical),
- Text(localizations.app_about(localizations.app_name)),
- ],
- );
- }
-
- void openGitHub(_) {
- launchUrl(
- Uri(
- scheme: 'https',
- host: 'github.com',
- path: 'maelchiotti/LocalMaterialNotes',
- ),
- );
- }
-
- void openLicense(_) {
- launchUrl(
- Uri(
- scheme: 'https',
- host: 'github.com',
- path: 'maelchiotti/LocalMaterialNotes/blob/main/LICENSE',
- ),
- );
- }
-
- void openIssues(_) {
- launchUrl(
- Uri(
- scheme: 'https',
- host: 'github.com',
- path: 'maelchiotti/LocalMaterialNotes/issues',
- ),
- );
- }
-}
diff --git a/lib/pages/settings/settings_main_page.dart b/lib/pages/settings/settings_main_page.dart
new file mode 100644
index 00000000..7fddb1ac
--- /dev/null
+++ b/lib/pages/settings/settings_main_page.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_settings_ui/flutter_settings_ui.dart';
+import 'package:go_router/go_router.dart';
+import 'package:localmaterialnotes/common/routing/router_route.dart';
+import 'package:localmaterialnotes/pages/settings/enums/settings_page.dart';
+import 'package:localmaterialnotes/pages/settings/widgets/custom_settings_list.dart';
+import 'package:localmaterialnotes/utils/constants/constants.dart';
+
+/// Page for the settings of the application.
+class SettingsMainPage extends ConsumerStatefulWidget {
+ const SettingsMainPage();
+
+ @override
+ ConsumerState createState() => _SettingsPageState();
+}
+
+class _SettingsPageState extends ConsumerState {
+ void _openSettingsPage(SettingsPage page) {
+ switch (page) {
+ case SettingsPage.appearance:
+ context.push(RouterRoute.settingsAppearance.fullPath!);
+ case SettingsPage.behavior:
+ context.push(RouterRoute.settingsBehavior.fullPath!);
+ case SettingsPage.editor:
+ context.push(RouterRoute.settingsEditor.fullPath!);
+ case SettingsPage.backup:
+ context.push(RouterRoute.settingsBackup.fullPath!);
+ case SettingsPage.about:
+ context.push(RouterRoute.settingsAbout.fullPath!);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return CustomSettingsList(
+ sections: [
+ SettingsSection(
+ tiles: [
+ SettingsTile.navigation(
+ leading: const Icon(Icons.palette),
+ title: Text(localizations.settings_appearance),
+ description: Text(localizations.settings_appearance_description),
+ onPressed: (_) => _openSettingsPage(SettingsPage.appearance),
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(Icons.swipe),
+ title: Text(localizations.settings_behavior),
+ description: Text(localizations.settings_behavior_description),
+ onPressed: (_) => _openSettingsPage(SettingsPage.behavior),
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(Icons.format_color_text),
+ title: Text(localizations.settings_editor),
+ description: Text(localizations.settings_editor_description),
+ onPressed: (_) => _openSettingsPage(SettingsPage.editor),
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(Icons.settings_backup_restore),
+ title: Text(localizations.settings_backup),
+ description: Text(localizations.settings_backup_description),
+ onPressed: (_) => _openSettingsPage(SettingsPage.backup),
+ ),
+ SettingsTile.navigation(
+ leading: const Icon(Icons.info),
+ title: Text(localizations.settings_about),
+ description: Text(localizations.settings_about_description),
+ onPressed: (_) => _openSettingsPage(SettingsPage.about),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart
deleted file mode 100644
index 1236e7ce..00000000
--- a/lib/pages/settings/settings_page.dart
+++ /dev/null
@@ -1,220 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:flutter_settings_ui/flutter_settings_ui.dart';
-import 'package:locale_names/locale_names.dart';
-import 'package:localmaterialnotes/pages/settings/settings_actions.dart';
-import 'package:localmaterialnotes/providers/notes/notes_provider.dart';
-import 'package:localmaterialnotes/utils/constants/constants.dart';
-import 'package:localmaterialnotes/utils/constants/paddings.dart';
-import 'package:localmaterialnotes/utils/extensions/string_extension.dart';
-import 'package:localmaterialnotes/utils/info_utils.dart';
-import 'package:localmaterialnotes/utils/preferences/confirmations.dart';
-import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
-import 'package:localmaterialnotes/utils/theme_utils.dart';
-import 'package:simple_icons/simple_icons.dart';
-
-class SettingsPage extends ConsumerStatefulWidget {
- const SettingsPage();
-
- @override
- ConsumerState createState() => _SettingsPageState();
-}
-
-class _SettingsPageState extends ConsumerState {
- final interactions = SettingsActions();
-
- bool showUndoRedoButtons = PreferenceKey.showUndoRedoButtons.getPreferenceOrDefault();
- bool showChecklistButton = PreferenceKey.showChecklistButton.getPreferenceOrDefault();
- bool showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault();
-
- bool showSeparators = PreferenceKey.showSeparators.getPreferenceOrDefault();
- bool showTilesBackground = PreferenceKey.showTilesBackground.getPreferenceOrDefault();
-
- @override
- Widget build(BuildContext context) {
- return SettingsList(
- platform: DevicePlatform.android,
- contentPadding: Paddings.custom.bottomSystemUi,
- lightTheme: SettingsThemeData(
- settingsListBackground: Theme.of(context).colorScheme.surface,
- ),
- darkTheme: SettingsThemeData(
- settingsListBackground: Theme.of(context).colorScheme.surface,
- ),
- sections: [
- SettingsSection(
- title: Text(localizations.settings_appearance),
- tiles: [
- SettingsTile.navigation(
- leading: const Icon(Icons.language),
- title: Text(localizations.settings_language),
- value: Text(Localizations.localeOf(context).nativeDisplayLanguage.capitalized),
- onPressed: interactions.selectLanguage,
- ),
- SettingsTile.navigation(
- leading: const Icon(Icons.palette),
- title: Text(localizations.settings_theme),
- value: Text(ThemeUtils().themeModeName),
- onPressed: (context) async {
- await interactions.selectTheme(context);
- setState(() {});
- },
- ),
- SettingsTile.switchTile(
- enabled: ThemeUtils().isDynamicThemingAvailable,
- leading: const Icon(Icons.bolt),
- title: Text(localizations.settings_dynamic_theming),
- description: Text(localizations.settings_dynamic_theming_description),
- initialValue: ThemeUtils().useDynamicTheming,
- onToggle: interactions.toggleDynamicTheming,
- ),
- SettingsTile.switchTile(
- enabled: ThemeUtils().brightness == Brightness.dark,
- leading: const Icon(Icons.nightlight),
- title: Text(localizations.settings_black_theming),
- description: Text(localizations.settings_black_theming_description),
- initialValue: ThemeUtils().useBlackTheming,
- onToggle: (toggled) {
- interactions.toggleBlackTheming(toggled);
- setState(() {});
- },
- ),
- ],
- ),
- SettingsSection(
- title: Text(localizations.settings_editor),
- tiles: [
- SettingsTile.switchTile(
- leading: const Icon(Icons.undo),
- title: Text(localizations.settings_show_undo_redo_buttons),
- description: Text(localizations.settings_show_undo_redo_buttons_description),
- initialValue: showUndoRedoButtons,
- onToggle: (toggled) {
- interactions.toggleBooleanSetting(PreferenceKey.showUndoRedoButtons, toggled);
- setState(() {
- showUndoRedoButtons = toggled;
- });
- },
- ),
- SettingsTile.switchTile(
- leading: const Icon(Icons.checklist),
- title: Text(localizations.settings_show_checklist_button),
- description: Text(localizations.settings_show_checklist_button_description),
- initialValue: showChecklistButton,
- onToggle: (toggled) {
- interactions.toggleBooleanSetting(PreferenceKey.showChecklistButton, toggled);
- setState(() {
- showChecklistButton = toggled;
- });
- },
- ),
- SettingsTile.switchTile(
- leading: const Icon(Icons.format_paint),
- title: Text(localizations.settings_show_toolbar),
- description: Text(localizations.settings_show_toolbar_description),
- initialValue: showToolbar,
- onToggle: (toggled) {
- interactions.toggleBooleanSetting(PreferenceKey.showToolbar, toggled);
- setState(() {
- showToolbar = toggled;
- });
- },
- ),
- ],
- ),
- SettingsSection(
- title: Text(localizations.settings_behavior),
- tiles: [
- SettingsTile.navigation(
- leading: const Icon(Icons.warning),
- title: Text(localizations.settings_confirmations),
- value: Text(Confirmations.fromPreference().title),
- onPressed: (context) async {
- await interactions.selectConfirmations(context);
- setState(() {});
- },
- ),
- SettingsTile.switchTile(
- leading: const Icon(Icons.safety_divider),
- title: Text(localizations.settings_show_separators),
- description: Text(localizations.settings_show_separators_description),
- initialValue: showSeparators,
- onToggle: (toggled) {
- interactions.toggleBooleanSetting(PreferenceKey.showSeparators, toggled);
- setState(() {
- showSeparators = toggled;
- });
- ref.invalidate(notesProvider); // Refresh the notes and bin pages
- },
- ),
- SettingsTile.switchTile(
- leading: const Icon(Icons.gradient),
- title: Text(localizations.settings_show_tiles_background),
- description: Text(localizations.settings_show_tiles_background_description),
- initialValue: showTilesBackground,
- onToggle: (toggled) {
- interactions.toggleBooleanSetting(PreferenceKey.showTilesBackground, toggled);
- setState(() {
- showTilesBackground = toggled;
- });
- ref.invalidate(notesProvider); // Refresh the notes and bin pages
- },
- ),
- ],
- ),
- SettingsSection(
- title: Text(localizations.settings_backup),
- tiles: [
- SettingsTile.navigation(
- leading: const Icon(SimpleIcons.json),
- title: Text(localizations.settings_export_json),
- value: Text(localizations.settings_export_json_description),
- onPressed: interactions.backupAsJson,
- ),
- SettingsTile.navigation(
- leading: const Icon(SimpleIcons.markdown),
- title: Text(localizations.settings_export_markdown),
- value: Text(localizations.settings_export_markdown_description),
- onPressed: interactions.backupAsMarkdown,
- ),
- SettingsTile.navigation(
- leading: const Icon(Icons.file_upload),
- title: Text(localizations.settings_import),
- value: Text(localizations.settings_import_description),
- onPressed: (context) => interactions.import(context, ref),
- ),
- ],
- ),
- SettingsSection(
- title: Text(localizations.settings_about),
- tiles: [
- SettingsTile(
- leading: const Icon(Icons.info),
- title: Text(localizations.app_name),
- value: Text(InfoUtils().appVersion),
- onPressed: interactions.showAbout,
- ),
- SettingsTile(
- leading: const Icon(SimpleIcons.github),
- title: Text(localizations.settings_github),
- value: Text(localizations.settings_github_description),
- onPressed: interactions.openGitHub,
- ),
- SettingsTile(
- leading: const Icon(Icons.balance),
- title: Text(localizations.settings_licence),
- value: Text(localizations.settings_licence_description),
- onPressed: interactions.openLicense,
- ),
- SettingsTile(
- leading: const Icon(Icons.bug_report),
- title: Text(localizations.settings_issue),
- value: Text(localizations.settings_issue_description),
- onPressed: interactions.openIssues,
- ),
- ],
- ),
- ],
- );
- }
-}
diff --git a/lib/pages/settings/widgets/custom_settings_list.dart b/lib/pages/settings/widgets/custom_settings_list.dart
new file mode 100644
index 00000000..73ef25cb
--- /dev/null
+++ b/lib/pages/settings/widgets/custom_settings_list.dart
@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_settings_ui/flutter_settings_ui.dart';
+import 'package:localmaterialnotes/utils/constants/paddings.dart';
+
+class CustomSettingsList extends StatelessWidget {
+ const CustomSettingsList({
+ super.key,
+ required this.sections,
+ });
+
+ final List sections;
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
+ return SettingsList(
+ platform: DevicePlatform.android,
+ contentPadding: Paddings.custom.bottomSystemUi,
+ lightTheme: SettingsThemeData(
+ settingsListBackground: theme.colorScheme.surface,
+ titleTextColor: theme.textTheme.bodyMedium?.color,
+ ),
+ darkTheme: SettingsThemeData(
+ settingsListBackground: theme.colorScheme.surface,
+ titleTextColor: theme.textTheme.bodyMedium?.color,
+ ),
+ sections: sections,
+ );
+ }
+}
diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart
deleted file mode 100644
index 8c947dd6..00000000
--- a/lib/providers/base_provider.dart
+++ /dev/null
@@ -1,5 +0,0 @@
-import 'package:localmaterialnotes/utils/database_utils.dart';
-
-mixin BaseProvider {
- late final databaseUtils = DatabaseUtils();
-}
diff --git a/lib/providers/bin/bin_provider.dart b/lib/providers/bin/bin_provider.dart
index b1211ab5..092ad891 100644
--- a/lib/providers/bin/bin_provider.dart
+++ b/lib/providers/bin/bin_provider.dart
@@ -2,13 +2,13 @@ import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:localmaterialnotes/models/note/note.dart';
-import 'package:localmaterialnotes/providers/base_provider.dart';
+import 'package:localmaterialnotes/utils/database_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'bin_provider.g.dart';
@riverpod
-class Bin extends _$Bin with BaseProvider {
+class Bin extends _$Bin {
@override
FutureOr> build() {
return get();
@@ -18,8 +18,8 @@ class Bin extends _$Bin with BaseProvider {
List notes = [];
try {
- notes = await databaseUtils.getAll(deleted: true);
- } on Exception catch (exception, stackTrace) {
+ notes = await DatabaseUtils().getAll(deleted: true);
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
}
@@ -36,7 +36,7 @@ class Bin extends _$Bin with BaseProvider {
Future empty() async {
try {
- await databaseUtils.emptyBin();
+ await DatabaseUtils().emptyBin();
} catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
@@ -49,7 +49,7 @@ class Bin extends _$Bin with BaseProvider {
Future permanentlyDelete(Note permanentlyDeletedNote) async {
try {
- await databaseUtils.delete(permanentlyDeletedNote);
+ await DatabaseUtils().delete(permanentlyDeletedNote);
} catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
@@ -69,8 +69,8 @@ class Bin extends _$Bin with BaseProvider {
}
try {
- await databaseUtils.deleteAll(notes);
- } on Exception catch (exception, stackTrace) {
+ await DatabaseUtils().deleteAll(notes);
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
}
@@ -87,8 +87,8 @@ class Bin extends _$Bin with BaseProvider {
restoredNote.deleted = false;
try {
- await databaseUtils.put(restoredNote);
- } on Exception catch (exception, stackTrace) {
+ await DatabaseUtils().put(restoredNote);
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
}
@@ -107,8 +107,8 @@ class Bin extends _$Bin with BaseProvider {
}
try {
- await databaseUtils.putAll(notes);
- } on Exception catch (exception, stackTrace) {
+ await DatabaseUtils().putAll(notes);
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
}
diff --git a/lib/providers/bin/bin_provider.g.dart b/lib/providers/bin/bin_provider.g.dart
index e41b2793..a0c5bee2 100644
--- a/lib/providers/bin/bin_provider.g.dart
+++ b/lib/providers/bin/bin_provider.g.dart
@@ -6,7 +6,7 @@ part of 'bin_provider.dart';
// RiverpodGenerator
// **************************************************************************
-String _$binHash() => r'318b346690c8597f9e87779f915ecf3a9bf62f6a';
+String _$binHash() => r'7a115c874d69d5ad6e75f0cf60972898e9a912c7';
/// See also [Bin].
@ProviderFor(Bin)
diff --git a/lib/providers/current_note/current_note_provider.dart b/lib/providers/current_note/current_note_provider.dart
deleted file mode 100644
index 6c8a7ec9..00000000
--- a/lib/providers/current_note/current_note_provider.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-import 'package:localmaterialnotes/models/note/note.dart';
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-
-part 'current_note_provider.g.dart';
-
-// ignore_for_file: use_setters_to_change_properties
-
-@Riverpod(keepAlive: true)
-class CurrentNote extends _$CurrentNote {
- @override
- Note? build() {
- return null;
- }
-
- void set(Note note) {
- state = note;
- }
-
- void reset() {
- state = null;
- }
-}
diff --git a/lib/providers/current_note/current_note_provider.g.dart b/lib/providers/current_note/current_note_provider.g.dart
deleted file mode 100644
index 7bd967be..00000000
--- a/lib/providers/current_note/current_note_provider.g.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'current_note_provider.dart';
-
-// **************************************************************************
-// RiverpodGenerator
-// **************************************************************************
-
-String _$currentNoteHash() => r'3820bcba7cfe8a1bd751d7fce008658a598e4b8f';
-
-/// See also [CurrentNote].
-@ProviderFor(CurrentNote)
-final currentNoteProvider = NotifierProvider.internal(
- CurrentNote.new,
- name: r'currentNoteProvider',
- debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$currentNoteHash,
- dependencies: null,
- allTransitiveDependencies: null,
-);
-
-typedef _$CurrentNote = Notifier;
-// ignore_for_file: type=lint
-// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/providers/editor_controller/editor_controller_provider.dart b/lib/providers/editor_controller/editor_controller_provider.dart
deleted file mode 100644
index a5b19b38..00000000
--- a/lib/providers/editor_controller/editor_controller_provider.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-import 'package:fleather/fleather.dart';
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-
-part 'editor_controller_provider.g.dart';
-
-// ignore_for_file: use_setters_to_change_properties
-
-@Riverpod(keepAlive: true)
-class EditorController extends _$EditorController {
- @override
- Raw? build() {
- return null;
- }
-
- void set(FleatherController fleatherController) {
- state = fleatherController;
- }
-
- void unset() {
- state = null;
- }
-}
diff --git a/lib/providers/editor_controller/editor_controller_provider.g.dart b/lib/providers/editor_controller/editor_controller_provider.g.dart
deleted file mode 100644
index 3048bc72..00000000
--- a/lib/providers/editor_controller/editor_controller_provider.g.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'editor_controller_provider.dart';
-
-// **************************************************************************
-// RiverpodGenerator
-// **************************************************************************
-
-String _$editorControllerHash() => r'daa52f679eec2218957f51064145f708efcbad46';
-
-/// See also [EditorController].
-@ProviderFor(EditorController)
-final editorControllerProvider = NotifierProvider.internal(
- EditorController.new,
- name: r'editorControllerProvider',
- debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$editorControllerHash,
- dependencies: null,
- allTransitiveDependencies: null,
-);
-
-typedef _$EditorController = Notifier;
-// ignore_for_file: type=lint
-// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/providers/layout/layout_provider.dart b/lib/providers/layout/layout_provider.dart
deleted file mode 100644
index 82537232..00000000
--- a/lib/providers/layout/layout_provider.dart
+++ /dev/null
@@ -1,18 +0,0 @@
-import 'package:localmaterialnotes/utils/preferences/layout.dart';
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-
-part 'layout_provider.g.dart';
-
-// ignore_for_file: use_setters_to_change_properties
-
-@Riverpod(keepAlive: true)
-class LayoutState extends _$LayoutState {
- @override
- Raw? build() {
- return Layout.fromPreference();
- }
-
- void set(Layout layout) {
- state = layout;
- }
-}
diff --git a/lib/providers/layout/layout_provider.g.dart b/lib/providers/layout/layout_provider.g.dart
deleted file mode 100644
index 3aa6090a..00000000
--- a/lib/providers/layout/layout_provider.g.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'layout_provider.dart';
-
-// **************************************************************************
-// RiverpodGenerator
-// **************************************************************************
-
-String _$layoutStateHash() => r'8b990b1d966c7acf39f878724ea773403b1205d8';
-
-/// See also [LayoutState].
-@ProviderFor(LayoutState)
-final layoutStateProvider = NotifierProvider.internal(
- LayoutState.new,
- name: r'layoutStateProvider',
- debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$layoutStateHash,
- dependencies: null,
- allTransitiveDependencies: null,
-);
-
-typedef _$LayoutState = Notifier;
-// ignore_for_file: type=lint
-// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/providers/notes/notes_provider.dart b/lib/providers/notes/notes_provider.dart
index d0ad0d58..7b7b8d0c 100644
--- a/lib/providers/notes/notes_provider.dart
+++ b/lib/providers/notes/notes_provider.dart
@@ -2,13 +2,13 @@ import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:localmaterialnotes/models/note/note.dart';
-import 'package:localmaterialnotes/providers/base_provider.dart';
+import 'package:localmaterialnotes/utils/database_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'notes_provider.g.dart';
@riverpod
-class Notes extends _$Notes with BaseProvider {
+class Notes extends _$Notes {
@override
FutureOr> build() {
return get();
@@ -18,8 +18,8 @@ class Notes extends _$Notes with BaseProvider {
List notes = [];
try {
- notes = await databaseUtils.getAll(deleted: false);
- } on Exception catch (exception, stackTrace) {
+ notes = await DatabaseUtils().getAll(deleted: false);
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
}
@@ -39,11 +39,11 @@ class Notes extends _$Notes with BaseProvider {
try {
if (editedNote.isEmpty) {
- await databaseUtils.delete(editedNote);
+ await DatabaseUtils().delete(editedNote);
} else {
- await databaseUtils.put(editedNote);
+ await DatabaseUtils().put(editedNote);
}
- } on Exception catch (exception, stackTrace) {
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
}
@@ -76,8 +76,8 @@ class Notes extends _$Notes with BaseProvider {
}
try {
- await databaseUtils.putAll(notes);
- } on Exception catch (exception, stackTrace) {
+ await DatabaseUtils().putAll(notes);
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
}
@@ -110,8 +110,8 @@ class Notes extends _$Notes with BaseProvider {
}
try {
- await databaseUtils.putAll(notes);
- } on Exception catch (exception, stackTrace) {
+ await DatabaseUtils().putAll(notes);
+ } catch (exception, stackTrace) {
log(exception.toString(), stackTrace: stackTrace);
return false;
}
diff --git a/lib/providers/notes/notes_provider.g.dart b/lib/providers/notes/notes_provider.g.dart
index e2ae1c11..3db4ad88 100644
--- a/lib/providers/notes/notes_provider.g.dart
+++ b/lib/providers/notes/notes_provider.g.dart
@@ -6,7 +6,7 @@ part of 'notes_provider.dart';
// RiverpodGenerator
// **************************************************************************
-String _$notesHash() => r'e80fc51bd8684e2e88b546d7d58d19aa9289e648';
+String _$notesHash() => r'35f1e8c7232c2411f82483f44b883cebdb522496';
/// See also [Notes].
@ProviderFor(Notes)
diff --git a/lib/providers/notifiers.dart b/lib/providers/notifiers.dart
new file mode 100644
index 00000000..06963e13
--- /dev/null
+++ b/lib/providers/notifiers.dart
@@ -0,0 +1,28 @@
+import 'package:fleather/fleather.dart';
+import 'package:flutter/material.dart';
+import 'package:localmaterialnotes/models/note/note.dart';
+import 'package:localmaterialnotes/utils/preferences/enums/layout.dart';
+import 'package:localmaterialnotes/utils/preferences/enums/swipe_action.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/utils/theme_utils.dart';
+
+final themeModeNotifier = ValueNotifier(ThemeUtils().themeMode);
+final dynamicThemingNotifier = ValueNotifier(ThemeUtils().useDynamicTheming);
+final blackThemingNotifier = ValueNotifier(ThemeUtils().useBlackTheming);
+
+final isSelectionModeNotifier = ValueNotifier(false);
+
+final layoutNotifier = ValueNotifier(Layout.fromPreference());
+final showTilesBackgroundNotifier = ValueNotifier(PreferenceKey.showTilesBackground.getPreferenceOrDefault());
+final showSeparatorsNotifier = ValueNotifier(PreferenceKey.showSeparators.getPreferenceOrDefault());
+
+final swipeActionsNotifier = ValueNotifier(
+ (SwipeAction.rightFromPreference(), SwipeAction.leftFromPreference()),
+);
+
+final currentNoteNotifier = ValueNotifier(null);
+
+final fleatherControllerNotifier = ValueNotifier(null);
+final fleatherControllerCanUndoNotifier = ValueNotifier(false);
+final fleatherControllerCanRedoNotifier = ValueNotifier(false);
+final fleatherFieldHasFocusNotifier = ValueNotifier(false);
diff --git a/lib/providers/selection_mode/selection_mode_provider.dart b/lib/providers/selection_mode/selection_mode_provider.dart
deleted file mode 100644
index 2c157a2d..00000000
--- a/lib/providers/selection_mode/selection_mode_provider.dart
+++ /dev/null
@@ -1,19 +0,0 @@
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-
-part 'selection_mode_provider.g.dart';
-
-@Riverpod(keepAlive: true)
-class SelectionMode extends _$SelectionMode {
- @override
- bool build() {
- return false;
- }
-
- void enterSelectionMode() {
- state = true;
- }
-
- void exitSelectionMode() {
- state = false;
- }
-}
diff --git a/lib/providers/selection_mode/selection_mode_provider.g.dart b/lib/providers/selection_mode/selection_mode_provider.g.dart
deleted file mode 100644
index 73c2f638..00000000
--- a/lib/providers/selection_mode/selection_mode_provider.g.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'selection_mode_provider.dart';
-
-// **************************************************************************
-// RiverpodGenerator
-// **************************************************************************
-
-String _$selectionModeHash() => r'ee4c15832eab0b67a50069888fbe232ed711b98f';
-
-/// See also [SelectionMode].
-@ProviderFor(SelectionMode)
-final selectionModeProvider = NotifierProvider.internal(
- SelectionMode.new,
- name: r'selectionModeProvider',
- debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$selectionModeHash,
- dependencies: null,
- allTransitiveDependencies: null,
-);
-
-typedef _$SelectionMode = Notifier;
-// ignore_for_file: type=lint
-// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/lib/utils/auto_export_utils.dart b/lib/utils/auto_export_utils.dart
new file mode 100644
index 00000000..02e79534
--- /dev/null
+++ b/lib/utils/auto_export_utils.dart
@@ -0,0 +1,116 @@
+import 'dart:io';
+
+import 'package:localmaterialnotes/utils/database_utils.dart';
+import 'package:localmaterialnotes/utils/extensions/uri_extension.dart';
+import 'package:localmaterialnotes/utils/files_utils.dart';
+import 'package:localmaterialnotes/utils/preferences/preference_key.dart';
+import 'package:localmaterialnotes/utils/preferences/preferences_utils.dart';
+import 'package:path/path.dart';
+import 'package:path_provider/path_provider.dart';
+
+/// Utilities for the auto export functionality.
+///
+/// This class is a singleton.
+class AutoExportUtils {
+ static final AutoExportUtils _singleton = AutoExportUtils._internal();
+
+ factory AutoExportUtils() {
+ return _singleton;
+ }
+
+ AutoExportUtils._internal();
+
+ /// Root directory where auto exports are located.
+ late Uri _autoExportDirectory;
+
+ /// Path to the default download directory on Android devices.
+ final _downloadDirectoryPath = '/storage/emulated/0/Download';
+
+ /// Subdirectories to add after the export path.
+ final subDirectories = ['Material Notes', 'backups'];
+
+ /// Precise directory where auto exports are located.
+ ///
+ /// It's a combination of [_autoExportDirectory] and [subDirectories].
+ Uri get backupsDirectory {
+ final backupsDirectoryPath = joinAll([_autoExportDirectory.path, ...subDirectories]);
+
+ return Uri.directory(backupsDirectoryPath);
+ }
+
+ Future ensureInitialized() async {
+ await _setAutoExportDirectory();
+
+ await performAutoExportIfNeeded();
+ }
+
+ /// Returns the JSON file in which to write the exported data.
+ Future get getAutoExportFile async {
+ return getExportFile(
+ backupsDirectory.toDecodedString,
+ 'json',
+ );
+ }
+
+ /// Set the auto export directory.
+ ///
+ /// By default, the auto export directory is the Android's Download directory.
+ /// If it doesn't exist, the application documents directory is used.
+ Future _setAutoExportDirectory() async {
+ final downloadsDirectory = Directory(_downloadDirectoryPath);
+
+ if (!downloadsDirectory.existsSync()) {
+ final externalStorageDirectory = await getApplicationDocumentsDirectory();
+
+ _autoExportDirectory = externalStorageDirectory.uri;
+ }
+
+ _autoExportDirectory = downloadsDirectory.uri;
+ }
+
+ /// Checks if an auto export should be performed.
+ ///
+ /// An auto export should be performed if it is enabled and either if:
+ /// - no auto export has been performed yet
+ /// - or the time difference between now and the last auto export is greater than the auto export frequency
+ /// chosen by the user
+ bool _shouldPerformAutoExport() {
+ final autoExportFrequency = PreferenceKey.autoExportFrequency.getPreferenceOrDefault();
+
+ // Auto export is disabled
+ if (autoExportFrequency == 0) {
+ return false;
+ }
+
+ final lastAutoExportDate = DateTime.tryParse(PreferenceKey.lastAutoExportDate.getPreferenceOrDefault());
+
+ // If the last auto export date is null, perform the auto first auto export now
+ if (lastAutoExportDate == null) {
+ return true;
+ }
+
+ final durationSinceLastAutoExport = DateTime.now().difference(lastAutoExportDate);
+ final autoExportFrequencyDuration = Duration(days: autoExportFrequency.toInt());
+
+ // If no auto export has been done for longer than the defined auto export frequency,
+ // then perform an auto export now
+ return durationSinceLastAutoExport > autoExportFrequencyDuration;
+ }
+
+ /// Performs an auto export of the database if it is needed.
+ Future performAutoExportIfNeeded() async {
+ if (!_shouldPerformAutoExport()) {
+ return;
+ }
+
+ final encrypt = PreferenceKey.autoExportEncryption.getPreferenceOrDefault();
+ final password = await PreferenceKey.autoExportPassword.getPreferenceOrDefaultSecure();
+
+ DatabaseUtils().autoExportAsJson(encrypt, password);
+
+ PreferencesUtils().set