From a7268c49fd9d15b4429a843d4dc1a6f49ef9a632 Mon Sep 17 00:00:00 2001 From: dbilgin Date: Sun, 8 Mar 2026 22:17:59 +0100 Subject: [PATCH 1/3] added quill --- lib/cubit/notes_cubit.dart | 5 +- lib/main.dart | 4 + lib/util/quill_utils.dart | 32 ++++ lib/widgets/details_form.dart | 120 ++++++------ lib/widgets/note_list.dart | 5 +- patches/flutter_code_editor+0.3.5.patch | 22 --- pubspec.lock | 233 ++++++++++++++++++------ pubspec.yaml | 4 +- 8 files changed, 288 insertions(+), 137 deletions(-) create mode 100644 lib/util/quill_utils.dart delete mode 100644 patches/flutter_code_editor+0.3.5.patch diff --git a/lib/cubit/notes_cubit.dart b/lib/cubit/notes_cubit.dart index 686c6c5..80867dc 100644 --- a/lib/cubit/notes_cubit.dart +++ b/lib/cubit/notes_cubit.dart @@ -7,6 +7,7 @@ import 'package:notelytask/cubit/local_folder_cubit.dart'; import 'package:notelytask/models/file_data.dart'; import 'package:notelytask/models/note.dart'; import 'package:notelytask/models/notes_state.dart'; +import 'package:notelytask/util/quill_utils.dart'; import 'package:notelytask/util/update_widget.dart'; import 'package:notelytask/utils.dart'; @@ -211,7 +212,9 @@ class NotesCubit extends HydratedCubit { final index = state.notes.indexWhere((element) => element.id == note.id); List updatedNotes = List.from(state.notes); - if (note.title.isEmpty && note.text.isEmpty && note.fileDataList.isEmpty) { + if (note.title.isEmpty && + note.fileDataList.isEmpty && + extractPlainTextFromDelta(note.text).trim().isEmpty) { if (index != -1) { updatedNotes.removeAt(index); emit(state.copyWith(notes: updatedNotes)); diff --git a/lib/main.dart b/lib/main.dart index efced4a..8be8c90 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:notelytask/screens/privacy_policy_page.dart'; import 'package:notelytask/screens/settings_page.dart'; import 'package:notelytask/service/navigation_service.dart'; import 'package:notelytask/theme.dart'; +import 'package:flutter_quill/flutter_quill.dart'; import 'package:path_provider/path_provider.dart'; import 'package:get_storage/get_storage.dart'; import 'package:get_it/get_it.dart'; @@ -72,6 +73,9 @@ class App extends StatelessWidget { return MaterialApp( title: 'NotelyTask', theme: getThemeData(settingsState.selectedTheme), + localizationsDelegates: const [ + FlutterQuillLocalizations.delegate, + ], navigatorKey: getIt().navigatorKey, initialRoute: '/', onGenerateRoute: (RouteSettings settings) { diff --git a/lib/util/quill_utils.dart b/lib/util/quill_utils.dart new file mode 100644 index 0000000..249f599 --- /dev/null +++ b/lib/util/quill_utils.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; + +/// Converts legacy plain/markdown text to Quill Delta JSON string. +/// If text is already a Delta JSON array, returns it unchanged. +String ensureQuillDelta(String text) { + if (text.isEmpty) return '[{"insert":"\\n"}]'; + try { + final decoded = jsonDecode(text); + if (decoded is List) return text; + } catch (_) {} + final content = text.endsWith('\n') ? text : '$text\n'; + return jsonEncode([{'insert': content}]); +} + +/// Extracts plain text from a Quill Delta JSON string for preview use. +String extractPlainTextFromDelta(String text) { + if (text.isEmpty) return ''; + try { + final decoded = jsonDecode(text); + if (decoded is List) { + final buf = StringBuffer(); + for (final op in decoded) { + if (op is Map && op['insert'] is String) buf.write(op['insert']); + } + final result = buf.toString(); + return result.endsWith('\n') + ? result.substring(0, result.length - 1) + : result; + } + } catch (_) {} + return text; +} diff --git a/lib/widgets/details_form.dart b/lib/widgets/details_form.dart index 1f22bd8..41bdb95 100644 --- a/lib/widgets/details_form.dart +++ b/lib/widgets/details_form.dart @@ -1,17 +1,16 @@ import 'dart:async'; +import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_code_editor/flutter_code_editor.dart'; -import 'package:highlight/languages/markdown.dart'; -import 'package:flutter_highlight/themes/atom-one-dark.dart'; -import 'package:flutter_highlight/themes/atom-one-light.dart'; +import 'package:flutter_quill/flutter_quill.dart'; import 'package:keyboard_detection/keyboard_detection.dart'; import 'package:notelytask/cubit/local_folder_cubit.dart'; import 'package:notelytask/cubit/notes_cubit.dart'; import 'package:notelytask/models/local_folder_state.dart'; import 'package:notelytask/models/note.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:notelytask/util/quill_utils.dart'; import 'package:notelytask/utils.dart'; import 'package:notelytask/widgets/file_list.dart'; @@ -34,42 +33,32 @@ class _DetailsFormState extends State { static KeyboardState keyboardState = KeyboardState.hidden; final _formKey = GlobalKey(); final _titleController = TextEditingController(); - late CodeController _codeController; + late QuillController _quillController; + late FocusNode _editorFocusNode; Timer? _debounce; - - Map _buildCustomTheme(bool isDark) { - final baseTheme = isDark ? atomOneDarkTheme : atomOneLightTheme; - return { - ...baseTheme, - 'section': TextStyle( - color: baseTheme['section']?.color ?? - (isDark ? const Color(0xFFE06C75) : const Color(0xFFE45649)), - fontSize: 20, - fontWeight: FontWeight.bold, - ), - 'strong': TextStyle( - color: baseTheme['strong']?.color, - fontWeight: FontWeight.bold, - ), - 'emphasis': TextStyle( - color: baseTheme['emphasis']?.color, - fontStyle: FontStyle.italic, - ), - }; - } + String _lastDelta = ''; @override void initState() { _titleController.text = widget.note.title; - _codeController = CodeController( - text: widget.note.text, - language: markdown, + final deltaJson = ensureQuillDelta(widget.note.text); + _lastDelta = deltaJson; + final doc = Document.fromJson( + List>.from(jsonDecode(deltaJson)), + ); + _quillController = QuillController( + document: doc, + selection: const TextSelection.collapsed(offset: 0), ); - _codeController.addListener(_onCodeChanged); + _editorFocusNode = FocusNode(); + _quillController.addListener(_onQuillChanged); super.initState(); } - void _onCodeChanged() { + void _onQuillChanged() { + final currentDelta = jsonEncode(_quillController.document.toDelta().toJson()); + if (currentDelta == _lastDelta) return; + _lastDelta = currentDelta; _submit(); } @@ -77,8 +66,9 @@ class _DetailsFormState extends State { void dispose() { _debounce?.cancel(); _titleController.dispose(); - _codeController.removeListener(_onCodeChanged); - _codeController.dispose(); + _quillController.removeListener(_onQuillChanged); + _quillController.dispose(); + _editorFocusNode.dispose(); super.dispose(); } @@ -101,7 +91,7 @@ class _DetailsFormState extends State { var note = Note( id: widget.note.id, title: _titleController.text, - text: _codeController.text, + text: jsonEncode(_quillController.document.toDelta().toJson()), date: DateTime.now(), isDeleted: widget.note.isDeleted, fileDataList: currentNote?.fileDataList ?? widget.note.fileDataList, @@ -116,11 +106,12 @@ class _DetailsFormState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; - final isDark = theme.brightness == Brightness.dark; + final isDocumentEmpty = + _quillController.document.toPlainText().trim().isEmpty; var shouldHideForm = widget.isDeletedList && _titleController.text.isEmpty && - _codeController.text.isEmpty; + isDocumentEmpty; return BlocListener( listener: (context, state) { @@ -182,24 +173,51 @@ class _DetailsFormState extends State { ), ), - const SizedBox(height: 16), + const SizedBox(height: 8), - // Content Field with live markdown syntax highlighting - Expanded( - child: CodeTheme( - data: CodeThemeData( - styles: _buildCustomTheme(isDark), + // Quill toolbar (hidden for deleted notes) + if (!widget.isDeletedList) + QuillSimpleToolbar( + controller: _quillController, + config: const QuillSimpleToolbarConfig( + showFontFamily: false, + showFontSize: false, + showInlineCode: false, + showSubscript: false, + showSuperscript: false, + showSearchButton: false, + showColorButton: false, + showBackgroundColorButton: false, ), - child: CodeField( - controller: _codeController, - textStyle: theme.textTheme.bodyLarge?.copyWith( - fontFamily: 'sans', - height: 1.5, + ), + + const SizedBox(height: 8), + + // Quill editor + Expanded( + child: IgnorePointer( + ignoring: widget.isDeletedList, + child: QuillEditor.basic( + controller: _quillController, + focusNode: _editorFocusNode, + config: QuillEditorConfig( + expands: true, + padding: EdgeInsets.zero, + autoFocus: false, + placeholder: 'Start writing...', + customStyles: DefaultStyles( + paragraph: DefaultTextBlockStyle( + theme.textTheme.bodyLarge?.copyWith( + height: 1.5, + ) ?? + const TextStyle(), + const HorizontalSpacing(0, 0), + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), + null, + ), + ), ), - gutterStyle: GutterStyle.none, - background: colorScheme.surface, - expands: true, - wrap: true, ), ), ), diff --git a/lib/widgets/note_list.dart b/lib/widgets/note_list.dart index e825c13..b995b63 100644 --- a/lib/widgets/note_list.dart +++ b/lib/widgets/note_list.dart @@ -6,6 +6,7 @@ import 'package:notelytask/cubit/settings_cubit.dart'; import 'package:notelytask/models/local_folder_state.dart'; import 'package:notelytask/models/note.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:notelytask/util/quill_utils.dart'; import 'package:notelytask/utils.dart'; class NoteList extends StatefulWidget { @@ -335,10 +336,10 @@ class _NoteListState extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (note.text.isNotEmpty) ...[ + if (extractPlainTextFromDelta(note.text).isNotEmpty) ...[ const SizedBox(height: 8), Text( - note.text, + extractPlainTextFromDelta(note.text), style: Theme.of(listContext) .textTheme .bodyMedium diff --git a/patches/flutter_code_editor+0.3.5.patch b/patches/flutter_code_editor+0.3.5.patch deleted file mode 100644 index 438aa7d..0000000 --- a/patches/flutter_code_editor+0.3.5.patch +++ /dev/null @@ -1,22 +0,0 @@ ---- a/lib/src/code_field/code_field.dart -+++ b/lib/src/code_field/code_field.dart -@@ -367,7 +367,7 @@ class _CodeFieldState extends State { - controller: widget.controller, - undoController: widget.undoController, - minLines: widget.minLines, -- maxLines: widget.maxLines, -+ maxLines: widget.wrap ? null : widget.maxLines, - expands: widget.expands, - scrollController: _codeScroll, - decoration: const InputDecoration( -@@ -396,6 +396,10 @@ class _CodeFieldState extends State { - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - // Control horizontal scrolling -+ if (widget.wrap) { -+ // When wrap is true, don't use horizontal scroll -+ return codeField; -+ } - return _wrapInScrollView(codeField, textStyle, constraints.maxWidth); - }, - ), diff --git a/pubspec.lock b/pubspec.lock index ae9d8dd..18162cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,14 +57,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.25" - autotrie: - dependency: transitive - description: - name: autotrie - sha256: "55da6faefb53cfcb0abb2f2ca8636123fb40e35286bb57440d2cf467568188f8" - url: "https://pub.dev" - source: hosted - version: "2.0.0" bloc: dependency: transitive description: @@ -265,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_quill_delta: + dependency: transitive + description: + name: dart_quill_delta + sha256: bddb0b2948bd5b5a328f1651764486d162c59a8ccffd4c63e8b2c5e44be1dac4 + url: "https://pub.dev" + source: hosted + version: "10.8.3" dart_style: dependency: transitive description: @@ -289,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" encrypt: dependency: "direct main" description: @@ -337,6 +345,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.3.7" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -366,22 +390,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_code_editor: - dependency: "direct main" + flutter_colorpicker: + dependency: transitive description: - name: flutter_code_editor - sha256: "9af48ba8e3558b6ea4bb98b84c5eb1649702acf53e61a84d88383eeb79b239b0" + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" url: "https://pub.dev" source: hosted - version: "0.3.5" - flutter_highlight: - dependency: "direct main" + version: "1.1.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_temp_fork: + dependency: transitive + description: + name: flutter_keyboard_visibility_temp_fork + sha256: e3d02900640fbc1129245540db16944a0898b8be81694f4bf04b6c985bed9048 + url: "https://pub.dev" + source: hosted + version: "0.1.5" + flutter_keyboard_visibility_windows: + dependency: transitive description: - name: flutter_highlight - sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -398,6 +454,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -406,6 +467,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.33" + flutter_quill: + dependency: "direct main" + description: + name: flutter_quill + sha256: b96bb8525afdeaaea52f5d02f525e05cc34acd176467ab6d6f35d434cf14fde2 + url: "https://pub.dev" + source: hosted + version: "11.5.0" + flutter_quill_delta_from_html: + dependency: transitive + description: + name: flutter_quill_delta_from_html + sha256: "0eb801ea8dd498cadc057507af5da794d4c9599ce58b2569cb3d4bb53ba8bed2" + url: "https://pub.dev" + source: hosted + version: "1.5.3" flutter_secure_storage: dependency: "direct main" description: @@ -576,14 +653,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - highlight: - dependency: "direct main" - description: - name: highlight - sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" - url: "https://pub.dev" - source: hosted - version: "0.7.0" hive: dependency: transitive description: @@ -648,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.7.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: @@ -736,14 +813,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - linked_scroll_controller: - dependency: transitive - description: - name: linked_scroll_controller - sha256: e6020062bcf4ffc907ee7fd090fa971e65d8dfaac3c62baf601a3ced0b37986a - url: "https://pub.dev" - source: hosted - version: "0.2.0" lints: dependency: transitive description: @@ -760,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" matcher: dependency: transitive description: @@ -800,14 +877,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.6.3" - mocktail: - dependency: transitive - description: - name: mocktail - sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" - url: "https://pub.dev" - source: hosted - version: "1.0.4" nested: dependency: transitive description: @@ -1048,22 +1117,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - rxdart: + quill_native_bridge: dependency: transitive description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + name: quill_native_bridge + sha256: "76a16512e398e84216f3f659f7cb18a89ec1e141ea908e954652b4ce6cf15b18" url: "https://pub.dev" source: hosted - version: "0.28.0" - scrollable_positioned_list: + version: "11.1.0" + quill_native_bridge_android: + dependency: transitive + description: + name: quill_native_bridge_android + sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569 + url: "https://pub.dev" + source: hosted + version: "0.0.1+2" + quill_native_bridge_ios: + dependency: transitive + description: + name: quill_native_bridge_ios + sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f + url: "https://pub.dev" + source: hosted + version: "0.0.1" + quill_native_bridge_macos: + dependency: transitive + description: + name: quill_native_bridge_macos + sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + quill_native_bridge_platform_interface: + dependency: transitive + description: + name: quill_native_bridge_platform_interface + sha256: "8264a2bdb8a294c31377a27b46c0f8717fa9f968cf113f7dc52d332ed9c84526" + url: "https://pub.dev" + source: hosted + version: "0.0.2+1" + quill_native_bridge_web: + dependency: transitive + description: + name: quill_native_bridge_web + sha256: "7c723f6824b0250d7f33e8b6c23f2f8eb0103fe48ee7ebf47ab6786b64d5c05d" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + quill_native_bridge_windows: + dependency: transitive + description: + name: quill_native_bridge_windows + sha256: "3f96ced19e3206ddf4f6f7dde3eb16bdd05e10294964009ea3a806d995aa7caa" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + quiver: dependency: transitive description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "0.3.8" + version: "3.2.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" share_plus: dependency: "direct main" description: @@ -1237,14 +1362,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 70e33c3..16b5d7b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,13 +36,11 @@ dependencies: keyboard_detection: ^0.8.1 encrypt: ^5.0.3 pinput: ^5.0.0 - flutter_code_editor: ^0.3.5 - highlight: ^0.7.0 + flutter_quill: ^11.0.0 flutter_widget_from_html: ^0.15.3 http_parser: ^4.0.2 flutter_secure_storage: ^9.2.2 package_info_plus: ^8.3.0 - flutter_highlight: ^0.7.0 permission_handler: ^11.3.1 dev_dependencies: From b8954ff654c4f8fdea45f4dafa39e9d3d9732688 Mon Sep 17 00:00:00 2001 From: dbilgin Date: Sun, 8 Mar 2026 22:20:06 +0100 Subject: [PATCH 2/3] remove selected note on start --- lib/models/settings_state.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/models/settings_state.dart b/lib/models/settings_state.dart index 79c8e5f..764ac78 100644 --- a/lib/models/settings_state.dart +++ b/lib/models/settings_state.dart @@ -14,7 +14,6 @@ class SettingsState extends Equatable { factory SettingsState.fromJson(Map json) { return SettingsState( - selectedNoteId: json['selectedNoteId'] as String?, selectedTheme: AppTheme.values.firstWhere( (theme) => theme.toString() == json['selectedTheme'], orElse: () => AppTheme.defaultDark, @@ -23,7 +22,6 @@ class SettingsState extends Equatable { } Map toJson() => { - 'selectedNoteId': selectedNoteId, 'selectedTheme': selectedTheme.toString(), }; From a17fdcc6e36ea2746715de0cf8ef502037cb6446 Mon Sep 17 00:00:00 2001 From: dbilgin Date: Sun, 8 Mar 2026 22:56:03 +0100 Subject: [PATCH 3/3] style and bump --- android/app/build.gradle | 2 +- debian/debian.yaml | 2 +- lib/screens/deleted_list_page.dart | 25 +- lib/screens/details_page.dart | 29 +- lib/screens/home_page.dart | 32 +- lib/screens/settings_page.dart | 24 +- lib/theme.dart | 283 +++++++------ lib/widgets/details_form.dart | 150 ++++--- lib/widgets/note_list.dart | 608 ++++++++++++++++------------ lib/widgets/note_list_detailed.dart | 51 ++- pubspec.lock | 8 + pubspec.yaml | 3 +- 12 files changed, 651 insertions(+), 566 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 9a590c2..c644b4b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,7 +25,7 @@ if (flutterVersionCode == null) { def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '4.0.0' + flutterVersionName = '5.0.1' } android { diff --git a/debian/debian.yaml b/debian/debian.yaml index 6b246e6..2792f22 100644 --- a/debian/debian.yaml +++ b/debian/debian.yaml @@ -6,7 +6,7 @@ flutter_app: control: Package: notelytask - Version: 4.0.0 + Version: 5.0.1 Architecture: amd64 Priority: optional Depends: libgtk-3-0, libblkid1, liblzma5 diff --git a/lib/screens/deleted_list_page.dart b/lib/screens/deleted_list_page.dart index 1d87316..9a11ac6 100644 --- a/lib/screens/deleted_list_page.dart +++ b/lib/screens/deleted_list_page.dart @@ -21,36 +21,15 @@ class _DeletedListPageState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: colorScheme.surface, appBar: AppBar( - backgroundColor: colorScheme.surface, - elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back_rounded), onPressed: () => Navigator.of(context).pop(), - color: colorScheme.onSurface, - ), - title: Row( - children: [ - Icon( - Icons.delete_rounded, - color: colorScheme.error, - size: 24, - ), - const SizedBox(width: 12), - Text( - 'Deleted Notes', - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - ], ), + title: const Text('Deleted Notes'), actions: [ BlocBuilder( builder: (context, state) { diff --git a/lib/screens/details_page.dart b/lib/screens/details_page.dart index f13df7d..72950fe 100644 --- a/lib/screens/details_page.dart +++ b/lib/screens/details_page.dart @@ -41,9 +41,6 @@ class _DetailsPageState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - var layout = SafeArea( child: DetailsForm( key: Key((note.id)), @@ -55,36 +52,16 @@ class _DetailsPageState extends State { if (widget.withAppBar) { return Scaffold( - backgroundColor: colorScheme.surface, appBar: AppBar( - backgroundColor: colorScheme.surface, - elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back_rounded), onPressed: () => Navigator.of(context).pop(), - color: colorScheme.onSurface, ), - title: Row( - children: [ - Icon( - widget.isDeletedList - ? Icons.visibility_rounded - : Icons.edit_note_rounded, - color: colorScheme.primary, - size: 24, - ), - const SizedBox(width: 12), - Text( - widget.isDeletedList ? 'View Note' : 'Edit Note', - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - ], + title: Text( + widget.isDeletedList ? 'View Note' : 'Edit Note', ), bottom: const PreferredSize( - preferredSize: Size(double.infinity, 4), + preferredSize: Size(double.infinity, 2), child: StateLoader(), ), ), diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart index 2214ec4..6f7333a 100644 --- a/lib/screens/home_page.dart +++ b/lib/screens/home_page.dart @@ -49,48 +49,26 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: colorScheme.surface, appBar: AppBar( - backgroundColor: colorScheme.surface, - elevation: 0, - title: Row( - children: [ - Image.asset( - 'assets/foreground.png', - width: 40, - height: 40, - color: colorScheme.primary, - ), - const SizedBox(width: 12), - Text( - 'NotelyTask', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - ], - ), + title: const Text('NotelyTask'), actions: [ IconButton( - icon: const Icon(Icons.delete_rounded), + icon: const Icon(Icons.delete_outline_rounded), tooltip: 'Deleted Notes', onPressed: _navigateToDeletedList, - color: colorScheme.onSurface, ), IconButton( icon: const Icon(Icons.settings_rounded), tooltip: 'Settings', onPressed: () => getIt().pushNamed('/settings'), - color: colorScheme.onSurface, ), + const SizedBox(width: 4), ], bottom: const PreferredSize( - preferredSize: Size(double.infinity, 4), + preferredSize: Size(double.infinity, 2), child: StateLoader(), ), ), diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 59a397c..dc80c97 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -21,7 +21,7 @@ class _SettingsPageState extends State { String version = ''; Future getVersion() async { final packageInfo = await PackageInfo.fromPlatform(); - return '${packageInfo.version}+${packageInfo.buildNumber}'; + return '${packageInfo.version}${packageInfo.buildNumber.isNotEmpty ? '+':''}${packageInfo.buildNumber}'; } @override @@ -62,20 +62,11 @@ class _SettingsPageState extends State { final colorScheme = theme.colorScheme; return Scaffold( - backgroundColor: colorScheme.surface, appBar: AppBar( - backgroundColor: colorScheme.surface, - title: Text( - 'Settings', - style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), + title: const Text('Settings'), leading: IconButton( icon: const Icon(Icons.arrow_back_rounded), onPressed: () => Navigator.of(context).pop(), - color: colorScheme.onSurface, ), ), body: SingleChildScrollView( @@ -89,7 +80,6 @@ class _SettingsPageState extends State { BlocBuilder( builder: (context, folderState) { return Card( - color: colorScheme.surface, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -248,7 +238,6 @@ class _SettingsPageState extends State { BlocBuilder( builder: (context, settingsState) { return Card( - color: colorScheme.surface, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -443,13 +432,12 @@ class _SettingsPageState extends State { Widget _buildSectionHeader(BuildContext context, String title) { final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - return Text( title, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w700, - color: colorScheme.onSurface, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.primary, + letterSpacing: 0.8, ), ); } diff --git a/lib/theme.dart b/lib/theme.dart index bd767d6..45a8adb 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:pinput/pinput.dart'; enum AppTheme { @@ -328,108 +329,8 @@ ThemeData getThemeData(AppTheme theme) { break; } - return ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - colorScheme: ColorScheme.dark( - primary: colors['primary']!, - primaryContainer: colors['primaryVariant']!, - secondary: colors['secondary']!, - secondaryContainer: colors['secondaryVariant']!, - surface: colors['surface']!, - surfaceContainerHighest: colors['surfaceVariant']!, - onPrimary: colors['onPrimary']!, - onSurface: colors['onSurface']!, - onSurfaceVariant: colors['onSurfaceVariant']!, - error: colors['error']!, - ), - scaffoldBackgroundColor: colors['background']!, - appBarTheme: AppBarTheme( - backgroundColor: colors['surface']!, - foregroundColor: colors['onSurface']!, - elevation: 0, - scrolledUnderElevation: 1, - surfaceTintColor: colors['primary']!, - titleTextStyle: TextStyle( - color: colors['onSurface']!, - fontSize: 20, - fontWeight: FontWeight.w600, - ), - iconTheme: IconThemeData( - color: colors['onSurface']!, - ), - ), - cardTheme: CardThemeData( - color: colors['surface']!, - elevation: 2, - shadowColor: Colors.black.withValues(alpha: 0.1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: colors['primary']!, - foregroundColor: colors['onPrimary']!, - elevation: 2, - shadowColor: colors['primary']!.withValues(alpha: 0.3), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - textStyle: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), - ), - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: colors['primary']!, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ), - iconButtonTheme: IconButtonThemeData( - style: IconButton.styleFrom( - foregroundColor: colors['onSurfaceVariant']!, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - floatingActionButtonTheme: FloatingActionButtonThemeData( - backgroundColor: colors['primary']!, - foregroundColor: colors['onPrimary']!, - elevation: 4, - shape: const CircleBorder(), - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: colors['surfaceVariant']!.withValues(alpha: 0.3), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colors['border']!), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colors['border']!), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colors['primary']!, width: 2), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: colors['error']!), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - hintStyle: TextStyle(color: colors['onSurfaceVariant']!), - labelStyle: TextStyle(color: colors['onSurfaceVariant']!), - ), - textTheme: TextTheme( + final interTextTheme = GoogleFonts.interTextTheme( + TextTheme( displayLarge: TextStyle( color: colors['onSurface']!, fontSize: 32, @@ -446,115 +347,241 @@ ThemeData getThemeData(AppTheme theme) { color: colors['onSurface']!, fontSize: 24, fontWeight: FontWeight.w600, + letterSpacing: -0.25, ), headlineLarge: TextStyle( color: colors['onSurface']!, fontSize: 22, fontWeight: FontWeight.w600, + letterSpacing: -0.15, ), headlineMedium: TextStyle( color: colors['onSurface']!, fontSize: 20, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, + letterSpacing: -0.1, ), headlineSmall: TextStyle( color: colors['onSurface']!, fontSize: 18, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, ), titleLarge: TextStyle( color: colors['onSurface']!, fontSize: 16, fontWeight: FontWeight.w600, + letterSpacing: 0.1, ), titleMedium: TextStyle( color: colors['onSurface']!, - fontSize: 14, + fontSize: 15, fontWeight: FontWeight.w500, + letterSpacing: 0.1, ), titleSmall: TextStyle( color: colors['onSurfaceVariant']!, - fontSize: 12, + fontSize: 13, fontWeight: FontWeight.w500, + letterSpacing: 0.1, ), bodyLarge: TextStyle( color: colors['onSurface']!, fontSize: 16, fontWeight: FontWeight.w400, - height: 1.5, + height: 1.6, ), bodyMedium: TextStyle( color: colors['onSurface']!, fontSize: 14, fontWeight: FontWeight.w400, - height: 1.4, + height: 1.5, ), bodySmall: TextStyle( color: colors['onSurfaceVariant']!, - fontSize: 12, + fontSize: 13, fontWeight: FontWeight.w400, - height: 1.3, + height: 1.4, ), labelLarge: TextStyle( color: colors['onSurface']!, fontSize: 14, fontWeight: FontWeight.w500, + letterSpacing: 0.1, ), labelMedium: TextStyle( color: colors['onSurfaceVariant']!, fontSize: 12, fontWeight: FontWeight.w500, + letterSpacing: 0.2, ), labelSmall: TextStyle( color: colors['onSurfaceVariant']!, - fontSize: 10, + fontSize: 11, fontWeight: FontWeight.w500, + letterSpacing: 0.3, + ), + ), + ); + + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: ColorScheme.dark( + primary: colors['primary']!, + primaryContainer: colors['primaryVariant']!, + secondary: colors['secondary']!, + secondaryContainer: colors['secondaryVariant']!, + surface: colors['surface']!, + surfaceContainerHighest: colors['surfaceVariant']!, + onPrimary: colors['onPrimary']!, + onSurface: colors['onSurface']!, + onSurfaceVariant: colors['onSurfaceVariant']!, + error: colors['error']!, + ), + scaffoldBackgroundColor: colors['background']!, + textTheme: interTextTheme, + appBarTheme: AppBarTheme( + backgroundColor: colors['background']!, + foregroundColor: colors['onSurface']!, + elevation: 0, + scrolledUnderElevation: 0, + surfaceTintColor: Colors.transparent, + titleTextStyle: GoogleFonts.inter( + color: colors['onSurface']!, + fontSize: 20, + fontWeight: FontWeight.w700, + letterSpacing: -0.3, + ), + iconTheme: IconThemeData( + color: colors['onSurfaceVariant']!, + size: 22, + ), + ), + cardTheme: CardThemeData( + color: colors['surface']!, + elevation: 0, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: colors['primary']!, + foregroundColor: colors['onPrimary']!, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + textStyle: GoogleFonts.inter( + fontWeight: FontWeight.w600, + fontSize: 14, + letterSpacing: 0.1, + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: colors['onSurface']!, + side: BorderSide(color: colors['border']!.withValues(alpha: 0.5)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + textStyle: GoogleFonts.inter( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: colors['primary']!, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + textStyle: GoogleFonts.inter( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + foregroundColor: colors['onSurfaceVariant']!, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: colors['primary']!, + foregroundColor: colors['onPrimary']!, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: false, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + errorBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + hintStyle: TextStyle( + color: colors['onSurfaceVariant']!.withValues(alpha: 0.5), ), ), dividerTheme: DividerThemeData( - color: colors['divider']!, - thickness: 1, - space: 1, + color: colors['onSurface']!.withValues(alpha: 0.08), + thickness: 0.5, + space: 0.5, ), snackBarTheme: SnackBarThemeData( - backgroundColor: colors['surface']!, - contentTextStyle: TextStyle(color: colors['onSurface']!), + backgroundColor: colors['surfaceVariant']!, + contentTextStyle: GoogleFonts.inter( + color: colors['onSurface']!, + fontSize: 14, + ), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(10), ), elevation: 4, ), dialogTheme: DialogThemeData( backgroundColor: colors['surface']!, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), ), - elevation: 8, + elevation: 0, ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: colors['surface']!, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - elevation: 8, + elevation: 0, ), listTileTheme: ListTileThemeData( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - titleTextStyle: TextStyle( + titleTextStyle: GoogleFonts.inter( color: colors['onSurface']!, - fontSize: 16, + fontSize: 15, fontWeight: FontWeight.w500, ), - subtitleTextStyle: TextStyle( + subtitleTextStyle: GoogleFonts.inter( color: colors['onSurfaceVariant']!, - fontSize: 14, + fontSize: 13, fontWeight: FontWeight.w400, ), ), textSelectionTheme: TextSelectionThemeData( cursorColor: colors['primary']!, - selectionColor: colors['primary']!, + selectionColor: colors['primary']!.withValues(alpha: 0.3), selectionHandleColor: colors['primary']!, ), ); @@ -565,14 +592,14 @@ final themeData = getThemeData(AppTheme.defaultDark); final defaultPinTheme = PinTheme( width: 56, height: 56, - textStyle: const TextStyle( + textStyle: GoogleFonts.inter( fontSize: 20, color: AppColors.onSurface, fontWeight: FontWeight.w600, ), decoration: BoxDecoration( color: AppColors.surfaceVariant.withValues(alpha: 0.3), - border: Border.all(color: AppColors.border), + border: Border.all(color: AppColors.border.withValues(alpha: 0.5)), borderRadius: BorderRadius.circular(12), ), ); diff --git a/lib/widgets/details_form.dart b/lib/widgets/details_form.dart index 41bdb95..585baea 100644 --- a/lib/widgets/details_form.dart +++ b/lib/widgets/details_form.dart @@ -56,7 +56,8 @@ class _DetailsFormState extends State { } void _onQuillChanged() { - final currentDelta = jsonEncode(_quillController.document.toDelta().toJson()); + final currentDelta = + jsonEncode(_quillController.document.toDelta().toJson()); if (currentDelta == _lastDelta) return; _lastDelta = currentDelta; _submit(); @@ -80,7 +81,6 @@ class _DetailsFormState extends State { if (_debounce?.isActive ?? false) _debounce?.cancel(); - // Get the current note from state to ensure we have the latest file list final currentNote = context .read() .state @@ -109,7 +109,7 @@ class _DetailsFormState extends State { final isDocumentEmpty = _quillController.document.toPlainText().trim().isEmpty; - var shouldHideForm = widget.isDeletedList && + final shouldHideForm = widget.isDeletedList && _titleController.text.isEmpty && isDocumentEmpty; @@ -131,7 +131,7 @@ class _DetailsFormState extends State { Icon( Icons.note_outlined, size: 64, - color: colorScheme.onSurfaceVariant, + color: colorScheme.onSurface.withValues(alpha: 0.12), ), const SizedBox(height: 16), Text( @@ -145,39 +145,60 @@ class _DetailsFormState extends State { ) : Form( key: _formKey, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Title Field - TextFormField( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title field — borderless, large + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: TextFormField( controller: _titleController, - onChanged: (text) => _submit(), + onChanged: (_) => _submit(), textInputAction: TextInputAction.next, style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, + letterSpacing: -0.3, ), decoration: InputDecoration( - hintText: 'Note title...', - hintStyle: TextStyle( - color: colorScheme.onSurfaceVariant, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, + hintText: 'Untitled', + hintStyle: + theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: -0.3, + color: colorScheme.onSurface + .withValues(alpha: 0.2), ), - filled: true, - fillColor: colorScheme.surface, - contentPadding: const EdgeInsets.all(16), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, ), ), + ), + + const SizedBox(height: 4), - const SizedBox(height: 8), + // Subtle divider + Divider( + height: 1, + thickness: 0.5, + indent: 20, + endIndent: 20, + color: + colorScheme.onSurface.withValues(alpha: 0.08), + ), - // Quill toolbar (hidden for deleted notes) - if (!widget.isDeletedList) - QuillSimpleToolbar( + // Toolbar (hidden for deleted notes) + if (!widget.isDeletedList) ...[ + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: colorScheme.onSurface + .withValues(alpha: 0.06), + width: 0.5, + ), + ), + ), + child: QuillSimpleToolbar( controller: _quillController, config: const QuillSimpleToolbarConfig( showFontFamily: false, @@ -190,42 +211,44 @@ class _DetailsFormState extends State { showBackgroundColorButton: false, ), ), + ), + ], - const SizedBox(height: 8), - - // Quill editor - Expanded( - child: IgnorePointer( - ignoring: widget.isDeletedList, - child: QuillEditor.basic( - controller: _quillController, - focusNode: _editorFocusNode, - config: QuillEditorConfig( - expands: true, - padding: EdgeInsets.zero, - autoFocus: false, - placeholder: 'Start writing...', - customStyles: DefaultStyles( - paragraph: DefaultTextBlockStyle( - theme.textTheme.bodyLarge?.copyWith( - height: 1.5, - ) ?? - const TextStyle(), - const HorizontalSpacing(0, 0), - const VerticalSpacing(0, 0), - const VerticalSpacing(0, 0), - null, - ), + // Quill editor + Expanded( + child: IgnorePointer( + ignoring: widget.isDeletedList, + child: QuillEditor.basic( + controller: _quillController, + focusNode: _editorFocusNode, + config: QuillEditorConfig( + expands: true, + padding: const EdgeInsets.fromLTRB( + 20, 12, 20, 16), + autoFocus: false, + placeholder: 'Start writing…', + customStyles: DefaultStyles( + paragraph: DefaultTextBlockStyle( + (theme.textTheme.bodyLarge?.copyWith( + height: 1.7, + )) ?? + const TextStyle(), + const HorizontalSpacing(0, 0), + const VerticalSpacing(0, 0), + const VerticalSpacing(0, 0), + null, ), ), ), ), ), - ], - ), + ), + ], ), ), ), + + // Bottom action bar KeyboardDetection( controller: KeyboardDetectionController( onChanged: (value) => setState(() => keyboardState = value)), @@ -236,18 +259,27 @@ class _DetailsFormState extends State { color: colorScheme.surface, border: Border( top: BorderSide( - color: colorScheme.onSurface.withValues(alpha: 0.1), + color: colorScheme.onSurface.withValues(alpha: 0.07), + width: 0.5, ), ), ), - padding: const EdgeInsets.all(16), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ IconButton( - icon: const Icon(Icons.attach_file_rounded), - tooltip: 'Upload File', + icon: Icon( + Icons.attach_file_rounded, + size: 20, + color: colorScheme.onSurfaceVariant, + ), + tooltip: 'Attach file', onPressed: () => uploadFile(context, widget.note), - color: colorScheme.onSurface, + style: IconButton.styleFrom( + minimumSize: const Size(36, 36), + padding: const EdgeInsets.all(8), + ), ), const Spacer(), FileList(noteId: widget.note.id), diff --git a/lib/widgets/note_list.dart b/lib/widgets/note_list.dart index b995b63..c08f76d 100644 --- a/lib/widgets/note_list.dart +++ b/lib/widgets/note_list.dart @@ -12,10 +12,12 @@ import 'package:notelytask/utils.dart'; class NoteList extends StatefulWidget { final List notes; final bool isDeletedList; + final String? selectedNoteId; const NoteList({ super.key, required this.notes, required this.isDeletedList, + this.selectedNoteId, }); @override @@ -51,7 +53,6 @@ class _NoteListState extends State { final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; - // For long press (mobile), show at center of screen final Offset menuPosition = position ?? Offset(MediaQuery.of(context).size.width / 2, MediaQuery.of(context).size.height / 2); @@ -69,11 +70,11 @@ class _NoteListState extends State { child: Row( children: [ Icon( - Icons.restore, + Icons.restore_rounded, color: Theme.of(context).colorScheme.primary, - size: 20, + size: 18, ), - const SizedBox(width: 12), + const SizedBox(width: 10), const Text('Restore'), ], ), @@ -83,11 +84,11 @@ class _NoteListState extends State { child: Row( children: [ Icon( - Icons.delete_forever, + Icons.delete_forever_rounded, color: Theme.of(context).colorScheme.error, - size: 20, + size: 18, ), - const SizedBox(width: 12), + const SizedBox(width: 10), const Text('Delete Permanently'), ], ), @@ -99,11 +100,11 @@ class _NoteListState extends State { child: Row( children: [ Icon( - Icons.delete, + Icons.delete_rounded, color: Theme.of(context).colorScheme.error, - size: 20, + size: 18, ), - const SizedBox(width: 12), + const SizedBox(width: 10), const Text('Delete'), ], ), @@ -154,282 +155,377 @@ class _NoteListState extends State { child: Column( children: [ if ((kIsWeb || isDesktop) && !widget.isDeletedList) - Container( - width: double.infinity, - margin: const EdgeInsets.only(bottom: 16), - child: ElevatedButton.icon( - onPressed: () => navigateToDetails( - context: context, - isDeletedList: widget.isDeletedList, - ), - icon: const Icon(Icons.add_rounded), - label: const Text('Create New Note'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), + _NewNoteButton( + onTap: () => navigateToDetails( + context: context, + isDeletedList: widget.isDeletedList, ), ), Expanded( child: widget.notes.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - widget.isDeletedList - ? Icons.delete_outline - : Icons.note_add_rounded, - size: 64, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(height: 16), - Text( - widget.isDeletedList - ? 'No deleted notes' - : 'No notes yet', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - const SizedBox(height: 8), - Text( - widget.isDeletedList - ? 'Deleted notes will appear here' - : 'Tap the + button to create your first note', - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - ], - ), - ) + ? _EmptyState(isDeletedList: widget.isDeletedList) : ListView.separated( + padding: EdgeInsets.zero, itemBuilder: (listContext, index) { final note = widget.notes[index]; - final fileData = note.fileDataList; - final fileNames = fileData.map((e) => e.name); - - return Dismissible( - key: ValueKey(note.date.millisecondsSinceEpoch), - onDismissed: (direction) => _dismissed(direction, note), - background: Container( - padding: const EdgeInsets.symmetric(horizontal: 24), - decoration: BoxDecoration( - color: widget.isDeletedList - ? const Color(0xFF10B981) - : const Color(0xFFEF4444), - borderRadius: BorderRadius.circular(12), - ), - alignment: Alignment.centerLeft, - child: Row( - children: [ - Icon( - widget.isDeletedList - ? Icons.restore - : Icons.delete, - color: Colors.white, - size: 24, - ), - const SizedBox(width: 8), - Text( - widget.isDeletedList ? 'Restore' : 'Delete', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - ], - ), + return _NoteCard( + note: note, + isDeletedList: widget.isDeletedList, + isSelected: note.id == widget.selectedNoteId, + onTap: () => navigateToDetails( + context: listContext, + note: note, + isDeletedList: widget.isDeletedList, ), - secondaryBackground: Container( - padding: const EdgeInsets.symmetric(horizontal: 24), - decoration: BoxDecoration( - color: const Color(0xFFEF4444), - borderRadius: BorderRadius.circular(12), - ), - alignment: Alignment.centerRight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, + onDismissed: (direction) => + _dismissed(direction, note), + onContextMenu: (position) => + _showContextMenu(listContext, position, note), + onLongPress: () => + _showContextMenu(listContext, null, note), + ); + }, + separatorBuilder: (_, __) => + const SizedBox(height: 10), + itemCount: widget.notes.length, + ), + ), + ], + ), + ); + } +} + +class _NewNoteButton extends StatelessWidget { + final VoidCallback onTap; + const _NewNoteButton({required this.onTap}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return GestureDetector( + onTap: onTap, + child: Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.symmetric(vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: colorScheme.primary.withValues(alpha: 0.35), + width: 1.5, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_rounded, color: colorScheme.primary, size: 18), + const SizedBox(width: 6), + Text( + 'New note', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + final bool isDeletedList; + const _EmptyState({required this.isDeletedList}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final theme = Theme.of(context); + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isDeletedList + ? Icons.delete_outline_rounded + : Icons.edit_note_rounded, + size: 72, + color: colorScheme.onSurface.withValues(alpha: 0.12), + ), + const SizedBox(height: 20), + Text( + isDeletedList ? 'No deleted notes' : 'No notes yet', + style: theme.textTheme.headlineSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + isDeletedList + ? 'Deleted notes will appear here' + : 'Tap + to create your first note', + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} + +class _NoteCard extends StatelessWidget { + final Note note; + final bool isDeletedList; + final bool isSelected; + final VoidCallback onTap; + final void Function(DismissDirection) onDismissed; + final void Function(Offset?) onContextMenu; + final VoidCallback onLongPress; + + const _NoteCard({ + required this.note, + required this.isDeletedList, + required this.isSelected, + required this.onTap, + required this.onDismissed, + required this.onContextMenu, + required this.onLongPress, + }); + + String _formatDate(DateTime date) { + final now = DateTime.now(); + final diff = now.difference(date); + if (diff.inDays >= 365) { + return '${(diff.inDays / 365).floor()}y'; + } else if (diff.inDays >= 30) { + return '${(diff.inDays / 30).floor()}mo'; + } else if (diff.inDays > 0) { + return '${diff.inDays}d'; + } else if (diff.inHours > 0) { + return '${diff.inHours}h'; + } else if (diff.inMinutes > 0) { + return '${diff.inMinutes}m'; + } else { + return 'now'; + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final preview = extractPlainTextFromDelta(note.text); + final hasContent = preview.isNotEmpty; + final fileCount = note.fileDataList.length; + + return Container( + decoration: BoxDecoration( + color: isSelected + ? colorScheme.primary.withValues(alpha: 0.08) + : colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: isSelected + ? Border.all(color: colorScheme.primary, width: 1.5) + : null, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.12), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Dismissible( + key: ValueKey(note.date.millisecondsSinceEpoch), + onDismissed: onDismissed, + background: _SwipeBackground( + color: isDeletedList + ? const Color(0xFF10B981) + : const Color(0xFFEF4444), + icon: isDeletedList ? Icons.restore_rounded : Icons.delete_rounded, + label: isDeletedList ? 'Restore' : 'Delete', + alignment: Alignment.centerLeft, + ), + secondaryBackground: _SwipeBackground( + color: const Color(0xFFEF4444), + icon: Icons.delete_forever_rounded, + label: 'Delete', + alignment: Alignment.centerRight, + ), + child: GestureDetector( + onSecondaryTapDown: (d) => onContextMenu(d.globalPosition), + onLongPress: onLongPress, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Left accent bar + Container( + width: 3, + color: hasContent + ? colorScheme.primary.withValues(alpha: 0.7) + : colorScheme.onSurface.withValues(alpha: 0.08), + ), + // Card content + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Delete', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 8), - const Icon( - Icons.delete_forever, - color: Colors.white, - size: 24, - ), - ], - ), - ), - child: Card( - margin: EdgeInsets.zero, - color: Theme.of(listContext) - .colorScheme - .surfaceContainerHighest, - elevation: 3, - shadowColor: Colors.black.withValues(alpha: 0.2), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - clipBehavior: Clip.antiAlias, - child: Builder( - builder: (cardContext) => GestureDetector( - onSecondaryTapDown: (details) => _showContextMenu( - listContext, - details.globalPosition, - note, - ), - onLongPress: () { - final RenderBox renderBox = - cardContext.findRenderObject() as RenderBox; - final position = renderBox.localToGlobal( - renderBox.size.center(Offset.zero), - ); - _showContextMenu( - listContext, - position, - note, - ); - }, - child: ListTile( - onTap: () => navigateToDetails( - context: listContext, - note: note, - isDeletedList: widget.isDeletedList, - ), - contentPadding: const EdgeInsets.all(16), - title: Text( - note.title.isEmpty - ? 'Untitled Note' - : note.title, - style: Theme.of(listContext) - .textTheme - .titleLarge - ?.copyWith( + // Title + date row + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + note.title.isEmpty + ? 'Untitled' + : note.title, + style: theme.textTheme.titleMedium + ?.copyWith( fontWeight: FontWeight.w600, color: note.title.isEmpty - ? Theme.of(listContext) - .colorScheme - .onSurfaceVariant - : Theme.of(listContext) - .colorScheme - .onSurface, + ? colorScheme.onSurfaceVariant + : colorScheme.onSurface, ), - maxLines: 1, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Text( + _formatDate(note.date), + style: theme.textTheme.labelSmall + ?.copyWith( + color: colorScheme.onSurfaceVariant + .withValues(alpha: 0.7), + ), + ), + ], + ), + // Preview + if (hasContent) ...[ + const SizedBox(height: 6), + Text( + preview, + style: theme.textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.4, + ), + maxLines: 2, overflow: TextOverflow.ellipsis, ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ], + // File badge + if (fileCount > 0) ...[ + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - if (extractPlainTextFromDelta(note.text).isNotEmpty) ...[ - const SizedBox(height: 8), - Text( - extractPlainTextFromDelta(note.text), - style: Theme.of(listContext) - .textTheme - .bodyMedium - ?.copyWith( - color: Theme.of(listContext) - .colorScheme - .onSurfaceVariant, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + Container( + padding: const EdgeInsets.symmetric( + horizontal: 7, vertical: 3), + decoration: BoxDecoration( + color: colorScheme.primary + .withValues(alpha: 0.12), + borderRadius: + BorderRadius.circular(6), ), - ], - if (fileNames.isNotEmpty) ...[ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Theme.of(listContext) - .colorScheme - .primary - .withValues(alpha: 0.1), - borderRadius: - BorderRadius.circular(6), - ), - child: Text( - '${fileNames.length} file${fileNames.length > 1 ? 's' : ''}', - style: Theme.of(listContext) - .textTheme - .labelSmall - ?.copyWith( - color: Theme.of(listContext) - .colorScheme - .primary, - fontWeight: FontWeight.w500, - ), - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.attach_file_rounded, + size: 11, + color: colorScheme.primary, + ), + const SizedBox(width: 3), + Text( + '$fileCount', + style: theme.textTheme.labelSmall + ?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ], ), - ], + ), ], ), - trailing: - _buildDateChip(listContext, note.date), - ), - ), + ], + ], ), ), - ); - }, - separatorBuilder: (context, index) => - const SizedBox(height: 12), - itemCount: widget.notes.length, + ), + ], ), + ), + ), + ), ), - ], + ), ), ); } +} - Widget _buildDateChip(BuildContext context, DateTime date) { - final now = DateTime.now(); - final difference = now.difference(date); +class _SwipeBackground extends StatelessWidget { + final Color color; + final IconData icon; + final String label; + final AlignmentGeometry alignment; - String timeText; - if (difference.inDays > 0) { - timeText = '${difference.inDays}d'; - } else if (difference.inHours > 0) { - timeText = '${difference.inHours}h'; - } else if (difference.inMinutes > 0) { - timeText = '${difference.inMinutes}m'; - } else { - timeText = 'now'; - } + const _SwipeBackground({ + required this.color, + required this.icon, + required this.label, + required this.alignment, + }); + @override + Widget build(BuildContext context) { + final isLeft = alignment == Alignment.centerLeft; return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest - .withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - timeText, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + color: color, + alignment: alignment, + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisSize: MainAxisSize.min, + children: isLeft + ? [ + Icon(icon, color: Colors.white, size: 20), + const SizedBox(width: 6), + Text(label, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14)), + ] + : [ + Text(label, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 14)), + const SizedBox(width: 6), + Icon(icon, color: Colors.white, size: 20), + ], ), ); } diff --git a/lib/widgets/note_list_detailed.dart b/lib/widgets/note_list_detailed.dart index 791b733..8bb48c4 100644 --- a/lib/widgets/note_list_detailed.dart +++ b/lib/widgets/note_list_detailed.dart @@ -24,40 +24,39 @@ class NoteListDetailed extends StatefulWidget { class _NoteListDetailedState extends State { @override Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - flex: 1, - child: NoteList( - notes: widget.notes, - isDeletedList: widget.isDeletedList, - ), - ), - BlocBuilder( - builder: (context, SettingsState state) { - String? selectedNoteId = state.selectedNoteId; - Note? existingNote = selectedNoteId != null - ? context - .read() - .state - .notes - .firstWhereOrNull((n) => n.id == selectedNoteId) - : null; + return BlocBuilder( + builder: (context, SettingsState state) { + final selectedNoteId = state.selectedNoteId; + Note? existingNote = selectedNoteId != null + ? context + .read() + .state + .notes + .firstWhereOrNull((n) => n.id == selectedNoteId) + : null; - return Expanded( - key: Key( - existingNote?.id ?? '', + return Row( + children: [ + Expanded( + flex: 1, + child: NoteList( + notes: widget.notes, + isDeletedList: widget.isDeletedList, + selectedNoteId: selectedNoteId, ), + ), + Expanded( + key: Key(existingNote?.id ?? ''), flex: 3, child: DetailsPage( note: existingNote, withAppBar: false, isDeletedList: widget.isDeletedList, ), - ); - }, - ), - ], + ), + ], + ); + }, ); } } diff --git a/pubspec.lock b/pubspec.lock index 18162cc..d3e1104 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -645,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 16b5d7b..a1f5fd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A simple notes application. publish_to: "none" -version: 4.0.0 +version: 5.0.1 environment: sdk: ">=3.3.3 <4.0.0" @@ -37,6 +37,7 @@ dependencies: encrypt: ^5.0.3 pinput: ^5.0.0 flutter_quill: ^11.0.0 + google_fonts: ^6.0.0 flutter_widget_from_html: ^0.15.3 http_parser: ^4.0.2 flutter_secure_storage: ^9.2.2