From b7187895e2675e15077acf163c592386596c39d0 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Mon, 14 Nov 2022 13:43:08 +0100 Subject: [PATCH 01/13] Implement basic filename templates --- lib/settings/settings.dart | 1 + lib/utils/note_filename_template.dart | 209 ++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 lib/utils/note_filename_template.dart diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index f6ea3f3b..664e4348 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -226,6 +226,7 @@ class NoteFileNameFormat extends GjSetting { Zettelkasten, DateOnly, KebabCase, + Template ]; static NoteFileNameFormat fromInternalString(String? str) => diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart new file mode 100644 index 00000000..cdb3bc60 --- /dev/null +++ b/lib/utils/note_filename_template.dart @@ -0,0 +1,209 @@ +import 'package:gitjournal/utils/datetime.dart'; +import 'package:intl/intl.dart'; + +final Map> validTemplateVariablesAndOptions = { + 'date': {'fmt'}, + 'title': { + 'lowercase', + 'uppercase', + 'snake_case', + 'kebab_case', + 'max_length', + 'default' + }, + 'uuidv4': {}, + 'root': {}, +}; + +class FileNameTemplate { + List<_TemplateSegment> segments; + + FileNameTemplate(this.segments); + + static FileNameTemplate parse(String template) { + try { + return FileNameTemplate(_parseTemplate(template)); + } on Exception catch (e) { + throw Exception("Problem parsing template: $e"); + } + } + + FileNameTemplateValidationResult validate() { + final segmentsIncludeTitle = + segments.any((segment) => segment.variableName == 'title'); + final segmentsIncludeDate = + segments.any((segment) => segment.variableName == 'date'); + final segmentsIncludeUuid = + segments.any((segment) => segment.variableName == 'uuidv4'); + if (!segmentsIncludeTitle && !segmentsIncludeDate && !segmentsIncludeUuid) { + return const FileNameTemplateValidationFailure( + "Template must include {{title}} or {{date}} or {{uuidv4}}"); + } + final segmentVariableErrors = segments.expand((segment) { + if (!segment.isVariable()) { + return []; + } + final optionNames = + Set.from(segment.variableOptions?.keys.toList() ?? []); + final validOptionNames = + validTemplateVariablesAndOptions[segment.variableName] ?? {}; + final invalidOptionNames = optionNames.difference(validOptionNames); + if (invalidOptionNames.isNotEmpty) { + return [ + "Invalid option(s) for variable ${segment.variableName}: ${invalidOptionNames.join(', ')}" + ]; + } + + return []; + }); + if (segmentVariableErrors.isNotEmpty) { + return FileNameTemplateValidationFailure(segmentVariableErrors.join(';')); + } + + return const FileNameTemplateValidationSuccess(); + } + + String render( + {required DateTime date, + required String root, + required String Function() uuidv4, + String? title}) { + final renderedSegments = segments.map((segment) { + if (segment.variableName == null) { + return segment.text; + } else if (segment.variableName == 'date') { + return _renderDate(date, segment.variableOptions); + } else if (segment.variableName == 'title') { + return _renderTitle(title, segment.variableOptions); + } else if (segment.variableName == 'uuidv4') { + return uuidv4(); + } else if (segment.variableName == 'root') { + return root; + } else { + throw Exception( + "Unknown template variable {{${segment.variableName}}}"); + } + }); + return renderedSegments.join(); + } +} + +abstract class FileNameTemplateValidationResult { + const FileNameTemplateValidationResult(); +} + +class FileNameTemplateValidationSuccess + extends FileNameTemplateValidationResult { + const FileNameTemplateValidationSuccess(); +} + +class FileNameTemplateValidationFailure + extends FileNameTemplateValidationResult { + const FileNameTemplateValidationFailure(this.message); + + final String message; +} + +List<_TemplateSegment> _parseTemplate(String template) { + final List<_TemplateSegment> segments = []; + var resolvingVariable = false; + + template.splitMapJoin( + RegExp("{{|}}"), + onMatch: (match) { + if (match[0] == '{{') { + if (resolvingVariable) { + throw Exception("Unexpected '{{' at ${match.start}"); + } + resolvingVariable = true; + } else { + if (!resolvingVariable) { + throw Exception("Unexpected '}}' at ${match.start}"); + } + resolvingVariable = false; + } + return ''; + }, + onNonMatch: (text) { + if (resolvingVariable) { + final variableSegments = text.split(':'); + final variableName = variableSegments[0]; + final optionsSegments = variableSegments[1].split(','); + final variableOptions = + Map.fromEntries(optionsSegments.map((optionText) { + final optionSegments = optionText.split('='); + if (optionSegments.length > 2) { + throw Exception("Invalid option for $variableName `$optionText`"); + } + return MapEntry( + optionSegments[0], + optionSegments.length == 1 ? 'true' : optionSegments[1], + ); + })); + segments.add(_TemplateSegment(text, variableName, variableOptions)); + } else { + segments.add(_TemplateSegment(text, null, null)); + } + return text; + }, + ); + + if (resolvingVariable) { + throw Exception("Unexpected end of template"); + } + + return segments; +} + +class _TemplateSegment { + final String text; + String? variableName; + Map? variableOptions; + + _TemplateSegment(this.text, this.variableName, this.variableOptions); + + isVariable() => variableName != null; + + validate() {} +} + +String _renderDate(DateTime date, Map? variableOptions) { + if (variableOptions == null || variableOptions['fmt'] == null) { + return toSimpleDateTime(date); + } + + return DateFormat(variableOptions['fmt']).format(date); +} + +String _renderTitle(String? titleInput, Map? variableOptions) { + final defaultTitle = variableOptions?['default'] ?? 'untitled'; + var title = titleInput ?? defaultTitle; + if (variableOptions == null) { + return title; + } + + if (variableOptions['lowercase'] == 'true') { + title = title.toLowerCase(); + } else if (variableOptions['uppercase'] == 'true') { + title = title.toUpperCase(); + } + + if (variableOptions['snake_case'] == 'true') { + title = title.replaceAll(RegExp(r'\s+'), '_'); + } else if (variableOptions['kebab_case'] == 'true') { + title = title.replaceAll(RegExp(r'\s+'), '-'); + } + + if (variableOptions['max_length'] != null) { + try { + final maxLength = int.parse(variableOptions['max_length']!); + if (title.length > maxLength) { + title = title.substring(0, maxLength); + } + } catch (e) { + throw Exception("Invalid max_length: ${variableOptions['max_length']}"); + } + } + + return title; +} From d04c1784ec905eb42e71458d0abd37b0b52c5e17 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Mon, 14 Nov 2022 14:06:30 +0100 Subject: [PATCH 02/13] Fix range error --- lib/utils/note_filename_template.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart index cdb3bc60..f2d91189 100644 --- a/lib/utils/note_filename_template.dart +++ b/lib/utils/note_filename_template.dart @@ -128,7 +128,9 @@ List<_TemplateSegment> _parseTemplate(String template) { if (resolvingVariable) { final variableSegments = text.split(':'); final variableName = variableSegments[0]; - final optionsSegments = variableSegments[1].split(','); + final List optionsSegments = + variableSegments.length == 1 ? [] : variableSegments[1].split(','); + final variableOptions = Map.fromEntries(optionsSegments.map((optionText) { final optionSegments = optionText.split('='); From 0adddb2727c6654e04e3a2c6301d1b1aae56c660 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Wed, 14 Dec 2022 16:51:47 +0100 Subject: [PATCH 03/13] Add tests --- lib/utils/note_filename_template.dart | 24 ++-- test/utils/note_filename_template_test.dart | 147 ++++++++++++++++++++ 2 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 test/utils/note_filename_template_test.dart diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart index f2d91189..5fbe16f3 100644 --- a/lib/utils/note_filename_template.dart +++ b/lib/utils/note_filename_template.dart @@ -12,7 +12,6 @@ final Map> validTemplateVariablesAndOptions = { 'default' }, 'uuidv4': {}, - 'root': {}, }; class FileNameTemplate { @@ -63,12 +62,23 @@ class FileNameTemplate { return const FileNameTemplateValidationSuccess(); } - String render( - {required DateTime date, - required String root, - required String Function() uuidv4, - String? title}) { + String render({ + required DateTime date, + required String root, + required String Function() uuidv4, + String? title, + }) { final renderedSegments = segments.map((segment) { + final invalidOptions = segment.variableOptions?.keys.where((key) { + final validOptions = + validTemplateVariablesAndOptions[segment.variableName]; + return validOptions != null ? validOptions.contains(key) : false; + }).toList(); + if (invalidOptions != null && invalidOptions.isNotEmpty) { + throw Exception( + "Invalid option(s) for variable ${segment.variableName}: ${invalidOptions.join(', ')}"); + } + if (segment.variableName == null) { return segment.text; } else if (segment.variableName == 'date') { @@ -77,8 +87,6 @@ class FileNameTemplate { return _renderTitle(title, segment.variableOptions); } else if (segment.variableName == 'uuidv4') { return uuidv4(); - } else if (segment.variableName == 'root') { - return root; } else { throw Exception( "Unknown template variable {{${segment.variableName}}}"); diff --git a/test/utils/note_filename_template_test.dart b/test/utils/note_filename_template_test.dart new file mode 100644 index 00000000..01a5d02b --- /dev/null +++ b/test/utils/note_filename_template_test.dart @@ -0,0 +1,147 @@ +import 'package:test/test.dart'; + +import 'package:gitjournal/utils/note_filename_template.dart'; + +void main() { + String renderTestTemplate(FileNameTemplate template, String? title) { + return template.render( + date: DateTime.parse('2022-02-27T19:00:00'), + root: 'root', + uuidv4: () => 'fake_uuid', + title: title, + ); + } + + group('valid FileNameTemplate', () { + test('combining date and title + multiple title options', () async { + final template = FileNameTemplate.parse( + '{{date:fmt=yyyy_MM_dd}}_{{title:lowercase,snake_case}}'); + + expect( + renderTestTemplate(template, "Some note title"), + '2022_02_27_some_note_title', + ); + }); + + test('title placeholder', () async { + final template = FileNameTemplate.parse('{{title}}'); + + expect( + renderTestTemplate(template, null), + "untitled", + ); + }); + + test('custom title placeholder', () async { + final template = + FileNameTemplate.parse('{{title:default=UNTITLED_NOTE}}'); + + expect( + renderTestTemplate(template, null), + "UNTITLED_NOTE", + ); + }); + + test('title length', () async { + final template = FileNameTemplate.parse('{{title:max_length=5}}'); + + expect( + renderTestTemplate(template, "Some note title"), + "Some ", + ); + }); + + test('kebab case title', () async { + final template = FileNameTemplate.parse('{{title:kebab_case}}'); + + expect( + renderTestTemplate(template, "Some note title with_underscores"), + "Some-note-title-with_underscores", + ); + }); + + test('snake case title', () async { + final template = FileNameTemplate.parse('{{title:snake_case}}'); + + expect( + renderTestTemplate(template, "Some note title with-hyphens"), + "Some_note_title_with-hyphens", + ); + }); + + test('uppercase title', () async { + final template = FileNameTemplate.parse('{{title:uppercase}}'); + + expect( + renderTestTemplate(template, "Some note title"), + "SOME NOTE TITLE", + ); + }); + + test('default date format', () { + final template = FileNameTemplate.parse('{{date}}'); + + expect( + renderTestTemplate(template, "Some note title"), + '2022-02-27-19-00-00', + ); + }); + + test('lowercase title', () async { + final template = FileNameTemplate.parse('{{title:lowercase}}'); + + expect( + renderTestTemplate(template, "Some note title"), + "some note title", + ); + }); + + test('custom date format', () { + final template = FileNameTemplate.parse('{{date:fmt=yyyy_MM_dd}}'); + + expect( + renderTestTemplate(template, "Some note title"), + '2022_02_27', + ); + }); + }); + + group('Invalid FileNameTemplate', () { + test('invalid date fmt', () { + final template = FileNameTemplate.parse('{{date:fmt=invalid format!!}}'); + + expect( + () => renderTestTemplate(template, "Some note title"), + throwsA(isA()), + ); + }); + + test('invalid template variable', () { + final template = FileNameTemplate.parse('{{invalid_template_variable}}'); + + expect( + () => renderTestTemplate(template, "Some note title"), + throwsA(isA()), + ); + }); + + test('invalid option name', () { + final template = FileNameTemplate.parse('{{title:invalid_option_name}}'); + + expect( + () => renderTestTemplate(template, "Some note title"), + throwsA(isA()), + ); + }); + + test('invalid template variable and option name', () { + final template = FileNameTemplate.parse( + '{{invalid_template_variable:invalid_option_name}}'); + + expect( + () => renderTestTemplate(template, "Some note title"), + throwsA(isA()), + ); + }); + }); +} From de7a6f82a0f73c4c1ecf163c5d5c71924b771564 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Wed, 14 Dec 2022 17:05:10 +0100 Subject: [PATCH 04/13] Add template filename option i10n l1t0n text --- lib/core/link.g.dart | 6 ------ lib/core/views/note_links_view.g.dart | 6 ------ lib/l10n.dart | 3 +++ lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fa.arb | 1 + lib/l10n/app_fi.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_hi.arb | 1 + lib/l10n/app_hu.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_it.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_ko.arb | 1 + lib/l10n/app_pl.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_pt_br.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_sv.arb | 1 + lib/l10n/app_ta.arb | 1 + lib/l10n/app_vi.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/app_zh_Hans.arb | 1 + lib/l10n/app_zh_TW.arb | 1 + lib/settings/settings.dart | 4 ++++ 26 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/core/link.g.dart b/lib/core/link.g.dart index c958444e..22b9b845 100644 --- a/lib/core/link.g.dart +++ b/lib/core/link.g.dart @@ -1,9 +1,3 @@ -/* - * SPDX-FileCopyrightText: 2019-2021 Vishesh Handa - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'link.dart'; diff --git a/lib/core/views/note_links_view.g.dart b/lib/core/views/note_links_view.g.dart index 5c26d833..835aaa01 100644 --- a/lib/core/views/note_links_view.g.dart +++ b/lib/core/views/note_links_view.g.dart @@ -1,9 +1,3 @@ -/* - * SPDX-FileCopyrightText: 2019-2021 Vishesh Handa - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'note_links_view.dart'; diff --git a/lib/l10n.dart b/lib/l10n.dart index e7d8edb2..1cde4cfe 100644 --- a/lib/l10n.dart +++ b/lib/l10n.dart @@ -76,6 +76,8 @@ extension LocalizedBuildContext on BuildContext { return loc.settingsNoteFileNameFormatZettelkasten; case Lk.settingsNoteFileNameFormatIso8601: return loc.settingsNoteFileNameFormatIso8601; + case Lk.settingsNoteFileNameFormatTemplate: + return loc.settingsNoteFileNameFormatTemplate; case Lk.settingsRemoteSyncManual: return loc.settingsRemoteSyncManual; case Lk.settingsRemoteSyncAuto: @@ -195,6 +197,7 @@ enum Lk { settingsNoteFileNameFormatTitle, settingsNoteFileNameFormatZettelkasten, settingsNoteFileNameFormatIso8601, + settingsNoteFileNameFormatTemplate, settingsRemoteSyncManual, settingsRemoteSyncAuto, widgetsFolderViewViewsStandard, diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index a37ea03c..a61dfa81 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 89ac2440..2659f0a9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index ee8e4d9b..f9e96653 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index cdd580d9..5ea5ae93 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 436152bd..5e5f8d75 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c96d462d..dcd9801b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index b74f3b2b..d99b9ab6 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 25de7a6f..622bd6d0 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index dd8264b0..5db59d43 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 626efb8c..b9cd5be6 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 205f66de..99ee3a46 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 8b593fa7..d926a8a3 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 834dccb2..0707be89 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 5592cb0a..120db3c4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_pt_br.arb b/lib/l10n/app_pt_br.arb index 553487fd..c48dfaf4 100644 --- a/lib/l10n/app_pt_br.arb +++ b/lib/l10n/app_pt_br.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 638991ee..c4de72dc 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -191,6 +191,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index e9b3e3f0..f53cca6d 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ta.arb b/lib/l10n/app_ta.arb index f8520922..0cf6f9fd 100644 --- a/lib/l10n/app_ta.arb +++ b/lib/l10n/app_ta.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 4f48ac53..0c0714df 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index cf3811f3..335b82d5 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb index ae69d0b9..06fb41e9 100644 --- a/lib/l10n/app_zh_Hans.arb +++ b/lib/l10n/app_zh_Hans.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_zh_TW.arb b/lib/l10n/app_zh_TW.arb index 3eb65bab..1387c377 100644 --- a/lib/l10n/app_zh_TW.arb +++ b/lib/l10n/app_zh_TW.arb @@ -187,6 +187,7 @@ "settingsNoteFileNameFormatSimple": "yyyy-mm-dd-hh-mm-ss", "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", + "settingsNoteFileNameFormatTemplate": "Template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 664e4348..d7a59cf2 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -211,6 +211,10 @@ class NoteFileNameFormat extends GjSetting { Lk.settingsNoteFileNameFormatKebabCase, "KebabCase", ); + static const Template = NoteFileNameFormat( + Lk.settingsNoteFileNameFormatTemplate, + "Template", + ); static const Default = FromTitle; From 7079591d257d088a9f7c63ae5db068c2bff8188f Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Wed, 14 Dec 2022 17:33:56 +0100 Subject: [PATCH 05/13] Refine template validation --- lib/utils/note_filename_template.dart | 22 +++++----- test/utils/note_filename_template_test.dart | 46 ++++++++++++--------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart index 5fbe16f3..c3e98486 100644 --- a/lib/utils/note_filename_template.dart +++ b/lib/utils/note_filename_template.dart @@ -53,6 +53,16 @@ class FileNameTemplate { ]; } + if (segmentsIncludeDate) { + final dateSegment = + segments.firstWhere((segment) => segment.variableName == 'date'); + try { + _renderDate(DateTime.now(), dateSegment.variableOptions); + } catch (e) { + return ["Invalid date format: ${dateSegment.text}"]; + } + } + return []; }); if (segmentVariableErrors.isNotEmpty) { @@ -69,16 +79,6 @@ class FileNameTemplate { String? title, }) { final renderedSegments = segments.map((segment) { - final invalidOptions = segment.variableOptions?.keys.where((key) { - final validOptions = - validTemplateVariablesAndOptions[segment.variableName]; - return validOptions != null ? validOptions.contains(key) : false; - }).toList(); - if (invalidOptions != null && invalidOptions.isNotEmpty) { - throw Exception( - "Invalid option(s) for variable ${segment.variableName}: ${invalidOptions.join(', ')}"); - } - if (segment.variableName == null) { return segment.text; } else if (segment.variableName == 'date') { @@ -173,8 +173,6 @@ class _TemplateSegment { _TemplateSegment(this.text, this.variableName, this.variableOptions); isVariable() => variableName != null; - - validate() {} } String _renderDate(DateTime date, Map? variableOptions) { diff --git a/test/utils/note_filename_template_test.dart b/test/utils/note_filename_template_test.dart index 01a5d02b..95234a43 100644 --- a/test/utils/note_filename_template_test.dart +++ b/test/utils/note_filename_template_test.dart @@ -1,18 +1,17 @@ -import 'package:test/test.dart'; - import 'package:gitjournal/utils/note_filename_template.dart'; +import 'package:test/test.dart'; void main() { - String renderTestTemplate(FileNameTemplate template, String? title) { - return template.render( - date: DateTime.parse('2022-02-27T19:00:00'), - root: 'root', - uuidv4: () => 'fake_uuid', - title: title, - ); - } - group('valid FileNameTemplate', () { + String renderTestTemplate(FileNameTemplate template, String? title) { + return template.render( + date: DateTime.parse('2022-02-27T19:00:00'), + root: 'root', + uuidv4: () => 'fake_uuid', + title: title, + ); + } + test('combining date and title + multiple title options', () async { final template = FileNameTemplate.parse( '{{date:fmt=yyyy_MM_dd}}_{{title:lowercase,snake_case}}'); @@ -107,12 +106,21 @@ void main() { }); group('Invalid FileNameTemplate', () { + test('no unique identifier in template', () { + final template = FileNameTemplate.parse('static_file_name'); + + expect( + template.validate(), + isA(), + ); + }); + test('invalid date fmt', () { final template = FileNameTemplate.parse('{{date:fmt=invalid format!!}}'); expect( - () => renderTestTemplate(template, "Some note title"), - throwsA(isA()), + template.validate(), + isA(), ); }); @@ -120,8 +128,8 @@ void main() { final template = FileNameTemplate.parse('{{invalid_template_variable}}'); expect( - () => renderTestTemplate(template, "Some note title"), - throwsA(isA()), + template.validate(), + isA(), ); }); @@ -129,8 +137,8 @@ void main() { final template = FileNameTemplate.parse('{{title:invalid_option_name}}'); expect( - () => renderTestTemplate(template, "Some note title"), - throwsA(isA()), + template.validate(), + isA(), ); }); @@ -139,8 +147,8 @@ void main() { '{{invalid_template_variable:invalid_option_name}}'); expect( - () => renderTestTemplate(template, "Some note title"), - throwsA(isA()), + template.validate(), + isA(), ); }); }); From c2966ba55409c53aefdf2b7e1579517e70c66828 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Wed, 14 Dec 2022 17:44:08 +0100 Subject: [PATCH 06/13] Validate title max_length --- lib/utils/note_filename_template.dart | 10 ++++++++++ test/utils/note_filename_template_test.dart | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart index c3e98486..5769ccf3 100644 --- a/lib/utils/note_filename_template.dart +++ b/lib/utils/note_filename_template.dart @@ -63,6 +63,16 @@ class FileNameTemplate { } } + if (segmentsIncludeTitle) { + final titleSegment = + segments.firstWhere((segment) => segment.variableName == 'title'); + try { + _renderTitle(titleSegment.text, titleSegment.variableOptions); + } catch (e) { + return ["Invalid title format: ${titleSegment.text}"]; + } + } + return []; }); if (segmentVariableErrors.isNotEmpty) { diff --git a/test/utils/note_filename_template_test.dart b/test/utils/note_filename_template_test.dart index 95234a43..0e212bc9 100644 --- a/test/utils/note_filename_template_test.dart +++ b/test/utils/note_filename_template_test.dart @@ -16,6 +16,7 @@ void main() { final template = FileNameTemplate.parse( '{{date:fmt=yyyy_MM_dd}}_{{title:lowercase,snake_case}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), '2022_02_27_some_note_title', @@ -25,6 +26,7 @@ void main() { test('title placeholder', () async { final template = FileNameTemplate.parse('{{title}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, null), "untitled", @@ -35,6 +37,7 @@ void main() { final template = FileNameTemplate.parse('{{title:default=UNTITLED_NOTE}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, null), "UNTITLED_NOTE", @@ -44,6 +47,7 @@ void main() { test('title length', () async { final template = FileNameTemplate.parse('{{title:max_length=5}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), "Some ", @@ -53,6 +57,7 @@ void main() { test('kebab case title', () async { final template = FileNameTemplate.parse('{{title:kebab_case}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title with_underscores"), "Some-note-title-with_underscores", @@ -62,6 +67,7 @@ void main() { test('snake case title', () async { final template = FileNameTemplate.parse('{{title:snake_case}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title with-hyphens"), "Some_note_title_with-hyphens", @@ -71,6 +77,7 @@ void main() { test('uppercase title', () async { final template = FileNameTemplate.parse('{{title:uppercase}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), "SOME NOTE TITLE", @@ -80,6 +87,7 @@ void main() { test('default date format', () { final template = FileNameTemplate.parse('{{date}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), '2022-02-27-19-00-00', @@ -89,6 +97,7 @@ void main() { test('lowercase title', () async { final template = FileNameTemplate.parse('{{title:lowercase}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), "some note title", @@ -98,6 +107,7 @@ void main() { test('custom date format', () { final template = FileNameTemplate.parse('{{date:fmt=yyyy_MM_dd}}'); + expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), '2022_02_27', @@ -142,6 +152,15 @@ void main() { ); }); + test('invalid title max_length value', () { + final template = FileNameTemplate.parse('{{title:max_length=qqq}}'); + + expect( + template.validate(), + isA(), + ); + }); + test('invalid template variable and option name', () { final template = FileNameTemplate.parse( '{{invalid_template_variable:invalid_option_name}}'); From c78d627adf9b2acca6972ed6225999833b6503ce Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Wed, 14 Dec 2022 17:48:06 +0100 Subject: [PATCH 07/13] Add malformed template test --- test/utils/note_filename_template_test.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/utils/note_filename_template_test.dart b/test/utils/note_filename_template_test.dart index 0e212bc9..da49bc1e 100644 --- a/test/utils/note_filename_template_test.dart +++ b/test/utils/note_filename_template_test.dart @@ -116,6 +116,15 @@ void main() { }); group('Invalid FileNameTemplate', () { + test("malformed template", () { + expect( + () { + FileNameTemplate.parse('{{date:fmt=yyyy_MM_dd'); + }, + throwsException, + ); + }); + test('no unique identifier in template', () { final template = FileNameTemplate.parse('static_file_name'); From 60f2985fd8fe3ad1bd3bdaa0e21ba0361e34a446 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Fri, 16 Dec 2022 19:22:47 +0100 Subject: [PATCH 08/13] Implement note filename template UI --- lib/core/folder/notes_folder_config.dart | 21 +- lib/core/folder/notes_folder_fs.dart | 3 + lib/core/note.dart | 44 +++- lib/core/note_storage.dart | 9 + lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fa.arb | 1 + lib/l10n/app_fi.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_hi.arb | 1 + lib/l10n/app_hu.arb | 1 + lib/l10n/app_id.arb | 1 + lib/l10n/app_it.arb | 1 + lib/l10n/app_ja.arb | 1 + lib/l10n/app_ko.arb | 1 + lib/l10n/app_pl.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_pt_br.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_sv.arb | 1 + lib/l10n/app_ta.arb | 1 + lib/l10n/app_vi.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/l10n/app_zh_Hans.arb | 1 + lib/l10n/app_zh_TW.arb | 1 + lib/settings/settings.dart | 19 +- lib/settings/settings_editors.dart | 26 +- lib/settings/settings_storage.dart | 22 +- .../settings_filename_format_preference.dart | 238 ++++++++++++++++++ .../widgets/settings_list_preference.dart | 28 ++- lib/utils/datetime.dart | 9 +- lib/utils/note_filename_template.dart | 159 +++++++++--- test/utils/note_filename_template_test.dart | 34 ++- 34 files changed, 535 insertions(+), 99 deletions(-) create mode 100644 lib/settings/widgets/settings_filename_format_preference.dart diff --git a/lib/core/folder/notes_folder_config.dart b/lib/core/folder/notes_folder_config.dart index a7da78ad..a7752825 100644 --- a/lib/core/folder/notes_folder_config.dart +++ b/lib/core/folder/notes_folder_config.dart @@ -5,9 +5,6 @@ */ import 'package:flutter/foundation.dart'; - -import 'package:shared_preferences/shared_preferences.dart'; - import 'package:gitjournal/core/folder/sorting_mode.dart'; import 'package:gitjournal/core/markdown/md_yaml_note_serializer.dart'; import 'package:gitjournal/core/notes/note.dart'; @@ -15,6 +12,7 @@ import 'package:gitjournal/editors/common_types.dart'; import 'package:gitjournal/folder_views/standard_view.dart'; import 'package:gitjournal/settings/settings.dart'; import 'package:gitjournal/settings/settings_sharedpref.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class NotesFolderConfig extends ChangeNotifier with SettingsSharedPref { NotesFolderConfig(this.id, this.pref); @@ -37,6 +35,8 @@ class NotesFolderConfig extends ChangeNotifier with SettingsSharedPref { var showNoteSummary = true; var fileNameFormat = NoteFileNameFormat.Default; var journalFileNameFormat = NoteFileNameFormat.Default; + var fileNameTemplate = "{{date}} {{title}}"; + var journalFileNameTemplate = "{{date}} {{title}}"; var yamlHeaderEnabled = true; @@ -64,6 +64,9 @@ class NotesFolderConfig extends ChangeNotifier with SettingsSharedPref { NoteFileNameFormat.fromInternalString(getString("noteFileNameFormat")); journalFileNameFormat = NoteFileNameFormat.fromInternalString( getString("journalNoteFileNameFormat")); + fileNameTemplate = getString("noteFileNameTemplate") ?? fileNameTemplate; + journalFileNameTemplate = + getString("journalNoteFileNameTemplate") ?? journalFileNameTemplate; yamlUnixTimestampMagnitude = NoteSerializationUnixTimestampMagnitude.fromInternalString( @@ -107,12 +110,18 @@ class NotesFolderConfig extends ChangeNotifier with SettingsSharedPref { Future save() async { var def = NotesFolderConfig(id, pref); - await setString("noteFileNameFormat", fileNameFormat.toInternalString(), - def.fileNameFormat.toInternalString()); + await setString( + "noteFileNameFormat", + NoteFileNameFormat.DateOnly.toInternalString(), + NoteFileNameFormat.DateOnly.toInternalString()); await setString( "journalNoteFileNameFormat", journalFileNameFormat.toInternalString(), def.journalFileNameFormat.toInternalString()); + await setString( + "noteFileNameTemplate", fileNameTemplate, def.fileNameTemplate); + await setString("journalNoteFileNameTemplate", journalFileNameTemplate, + def.journalFileNameTemplate); await setString("sortingField", sortingField.toInternalString(), def.sortingField.toInternalString()); @@ -162,6 +171,8 @@ class NotesFolderConfig extends ChangeNotifier with SettingsSharedPref { return { "noteFileNameFormat": fileNameFormat.toInternalString(), "journalNoteFileNameFormat": journalFileNameFormat.toInternalString(), + "noteFileNameTemplate": fileNameTemplate, + "journalNoteFileNameTemplate": journalFileNameTemplate, "yamlModifiedKey": yamlModifiedKey, "yamlCreatedKey": yamlCreatedKey, "yamlTagsKey": yamlTagsKey, diff --git a/lib/core/folder/notes_folder_fs.dart b/lib/core/folder/notes_folder_fs.dart index 4d093926..b41bbd0c 100644 --- a/lib/core/folder/notes_folder_fs.dart +++ b/lib/core/folder/notes_folder_fs.dart @@ -36,6 +36,9 @@ class NotesFolderFS with NotesFolderNotifier implements NotesFolder { final NotesFolderConfig _config; late final FileStorage fileStorage; + get fileNameTemplate => config.fileNameTemplate; + get journalFileNameTemplate => _config.journalFileNameTemplate; + NotesFolderFS(NotesFolderFS parent, this._folderPath, this._config) : _parent = parent, fileStorage = parent.fileStorage { diff --git a/lib/core/note.dart b/lib/core/note.dart index bb4152f6..5d4a6cfc 100644 --- a/lib/core/note.dart +++ b/lib/core/note.dart @@ -7,18 +7,19 @@ import 'package:collection/collection.dart'; import 'package:dart_date/dart_date.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:recase/recase.dart'; -import 'package:universal_io/io.dart' as io; -import 'package:uuid/uuid.dart'; - import 'package:gitjournal/core/folder/notes_folder_fs.dart'; import 'package:gitjournal/error_reporting.dart'; import 'package:gitjournal/generated/core.pb.dart' as pb; import 'package:gitjournal/logger/logger.dart'; import 'package:gitjournal/settings/settings.dart'; import 'package:gitjournal/utils/datetime.dart'; +import 'package:gitjournal/utils/note_filename_template.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:recase/recase.dart'; +import 'package:universal_io/io.dart' as io; +import 'package:uuid/uuid.dart'; + import 'file/file.dart'; import 'markdown/md_yaml_doc.dart'; import 'markdown/md_yaml_note_serializer.dart'; @@ -242,6 +243,8 @@ class Note implements File { var format = !isJournal ? parent.config.fileNameFormat : parent.config.journalFileNameFormat; + Log.i("title: " + (title ?? "null")); + switch (format) { case NoteFileNameFormat.SimpleDate: return toSimpleDateTime(date); @@ -265,8 +268,14 @@ class Note implements File { case NoteFileNameFormat.Zettelkasten: return toZettleDateTime(date); case NoteFileNameFormat.KebabCase: - title ??= toSimpleDateTime(date); - return buildKebabTitleFileName(parent.fullFolderPath, title, ext); + case NoteFileNameFormat.Template: + return buildTemplateFileName( + isJournal ? parent.journalFileNameTemplate : parent.fileNameTemplate, + date, + parent.fullFolderPath, + title, + ext, + ); } return date.toString(); @@ -356,7 +365,7 @@ class Note implements File { bool get shouldRebuildPath { if (fileFormat != NoteFileFormat.Markdown) return false; - return parent.config.fileNameFormat == NoteFileNameFormat.FromTitle; + return parent.config.fileNameFormat.usesTitle; } String get body => _body; @@ -565,3 +574,20 @@ String buildKebabTitleFileName(String parentDir, String title, String ext) { return ensureFileNameUnique(parentDir, title.camelCase, ext); } + +String buildTemplateFileName( + String template, + DateTime date, + String parentDir, + String? title, + String ext, +) { + // Sanitize the title - these characters are not allowed in Windows + title = FileNameTemplate.parse( + template, + ) + .render(date: date, uuidv4: const Uuid().v4, title: title) + .replaceAll(RegExp(r'[/<\>":|?*]'), '_'); + + return ensureFileNameUnique(parentDir, title, ext); +} diff --git a/lib/core/note_storage.dart b/lib/core/note_storage.dart index 3e6f0dff..87a06b0d 100644 --- a/lib/core/note_storage.dart +++ b/lib/core/note_storage.dart @@ -13,7 +13,9 @@ import 'package:gitjournal/core/markdown/md_yaml_doc_codec.dart'; import 'package:gitjournal/core/markdown/md_yaml_doc_loader.dart'; import 'package:gitjournal/core/markdown/md_yaml_note_serializer.dart'; import 'package:gitjournal/logger/logger.dart'; +import 'package:gitjournal/utils/result.dart'; import 'package:path/path.dart' as p; +import 'package:path/path.dart'; import 'package:universal_io/io.dart' as io; import 'file/file.dart'; @@ -53,6 +55,13 @@ class NoteStorage { assert(note.fullFilePath.startsWith(p.separator)); + final directory = dirname(note.fullFilePath); + final directoryExists = io.Directory(directory).existsSync(); + if (!directoryExists) { + Log.i("msg: Directory does not exist, creating it: $directory"); + await (io.Directory(directory).create(recursive: true)); + } + var file = io.File(note.fullFilePath); await file.writeAsBytes(contents, flush: true); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index a61dfa81..552cb54a 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2659f0a9..d2f35658 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f9e96653..a399b68f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 5ea5ae93..30ee8100 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 5e5f8d75..d86b9784 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index dcd9801b..e5da6783 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index d99b9ab6..76fe762e 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index 622bd6d0..bea2defe 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 5db59d43..87322c58 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b9cd5be6..511ba3c5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 99ee3a46..1573cc9d 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index d926a8a3..3a612589 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0707be89..bb73b6e4 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 120db3c4..7221a9a6 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_pt_br.arb b/lib/l10n/app_pt_br.arb index c48dfaf4..ad8f935e 100644 --- a/lib/l10n/app_pt_br.arb +++ b/lib/l10n/app_pt_br.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index c4de72dc..38cfcf61 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -192,6 +192,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index f53cca6d..613fef54 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_ta.arb b/lib/l10n/app_ta.arb index 0cf6f9fd..1072d7b0 100644 --- a/lib/l10n/app_ta.arb +++ b/lib/l10n/app_ta.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 0c0714df..683576f9 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 335b82d5..610a2988 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb index 06fb41e9..6744cd66 100644 --- a/lib/l10n/app_zh_Hans.arb +++ b/lib/l10n/app_zh_Hans.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/l10n/app_zh_TW.arb b/lib/l10n/app_zh_TW.arb index 1387c377..8b4724b0 100644 --- a/lib/l10n/app_zh_TW.arb +++ b/lib/l10n/app_zh_TW.arb @@ -188,6 +188,7 @@ "settingsNoteFileNameFormatDateOnly": "yyyy-mm-dd", "settingsNoteFileNameFormatKebabCase": "Kebab Case", "settingsNoteFileNameFormatTemplate": "Template", + "settingsNoteFileNameFormatTemplateDialogTitle": "Edit new note filename template", "settingsHomeScreenAllNotes": "All Notes", "settingsHomeScreenAllFolders": "All Folders", "settingsEditorDefaultViewEdit": "Edit", diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index d7a59cf2..8ad9fa1f 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -177,6 +177,8 @@ class Settings extends ChangeNotifier with SettingsSharedPref { } class NoteFileNameFormat extends GjSetting { + final bool usesTitle; + static const Iso8601WithTimeZone = NoteFileNameFormat( Lk.settingsNoteFileNameFormatIso8601WithTimeZone, "Iso8601WithTimeZone", @@ -190,9 +192,8 @@ class NoteFileNameFormat extends GjSetting { "Iso8601WithTimeZoneWithoutColon", ); static const FromTitle = NoteFileNameFormat( - Lk.settingsNoteFileNameFormatTitle, - "FromTitle", - ); + Lk.settingsNoteFileNameFormatTitle, "FromTitle", + usesTitle: true); static const SimpleDate = NoteFileNameFormat( Lk.settingsNoteFileNameFormatSimple, "SimpleDate", @@ -208,17 +209,15 @@ class NoteFileNameFormat extends GjSetting { "DateOnly", ); static const KebabCase = NoteFileNameFormat( - Lk.settingsNoteFileNameFormatKebabCase, - "KebabCase", - ); + Lk.settingsNoteFileNameFormatKebabCase, "KebabCase", + usesTitle: true); static const Template = NoteFileNameFormat( - Lk.settingsNoteFileNameFormatTemplate, - "Template", - ); + Lk.settingsNoteFileNameFormatTemplate, "Template", + usesTitle: true); static const Default = FromTitle; - const NoteFileNameFormat(super.lk, super.str); + const NoteFileNameFormat(super.lk, super.str, {this.usesTitle = false}); static const options = [ SimpleDate, diff --git a/lib/settings/settings_editors.dart b/lib/settings/settings_editors.dart index bcaf6f92..11fdad8d 100644 --- a/lib/settings/settings_editors.dart +++ b/lib/settings/settings_editors.dart @@ -12,6 +12,7 @@ import 'package:gitjournal/editors/common_types.dart'; import 'package:gitjournal/l10n.dart'; import 'package:gitjournal/settings/settings.dart'; import 'package:gitjournal/settings/settings_storage.dart'; +import 'package:gitjournal/settings/widgets/settings_filename_format_preference.dart'; import 'package:gitjournal/settings/widgets/settings_header.dart'; import 'package:gitjournal/settings/widgets/settings_list_preference.dart'; import 'package:gitjournal/utils/utils.dart'; @@ -83,7 +84,7 @@ class SettingsEditorsScreenState extends State { ), ProOverlay( child: SwitchListTile( - title: Text(context.loc.singleJournalEntry), + title: Text(context.loc.featureSingleJournalEntry), value: settings.journalEditorSingleNote, onChanged: (bool newVal) { settings.journalEditorSingleNote = newVal; @@ -93,20 +94,23 @@ class SettingsEditorsScreenState extends State { ), ), ProOverlay( - child: ListPreference( - title: context.loc.settingsNoteNewNoteFileName, - currentOption: - folderConfig.journalFileNameFormat.toPublicString(context), - options: NoteFileNameFormat.options - .map((f) => f.toPublicString(context)) - .toList(), - onChange: (String publicStr) { - var format = - NoteFileNameFormat.fromPublicString(context, publicStr); + child: NoteFileNameFormatPreference( + getFileNameFormatFromConfig: () => folderConfig.journalFileNameFormat, + setFileNameFormatInConfig: (format) { folderConfig.journalFileNameFormat = format; folderConfig.save(); setState(() {}); }, + getFileNameTemplateFromConfig: () => + folderConfig.journalFileNameTemplate.isEmpty + ? "{{date}} {{title}}" + : folderConfig.journalFileNameTemplate, + setFileNameFormatWithTemplateInConfig: (text) { + folderConfig.journalFileNameFormat = NoteFileNameFormat.Template; + folderConfig.journalFileNameTemplate = text; + folderConfig.save(); + setState(() {}); + }, ), ), ]); diff --git a/lib/settings/settings_storage.dart b/lib/settings/settings_storage.dart index 4a07ef90..6a3dac00 100644 --- a/lib/settings/settings_storage.dart +++ b/lib/settings/settings_storage.dart @@ -19,6 +19,7 @@ import 'package:gitjournal/settings/settings_images.dart'; import 'package:gitjournal/settings/settings_note_metadata.dart'; import 'package:gitjournal/settings/settings_tags.dart'; import 'package:gitjournal/settings/storage_config.dart'; +import 'package:gitjournal/settings/widgets/settings_filename_format_preference.dart'; import 'package:gitjournal/settings/widgets/settings_header.dart'; import 'package:gitjournal/settings/widgets/settings_list_preference.dart'; import 'package:gitjournal/utils/utils.dart'; @@ -44,18 +45,21 @@ class SettingsStorageScreen extends StatelessWidget { var list = ListView( children: [ - ListPreference( - title: context.loc.settingsNoteNewNoteFileName, - currentOption: folderConfig.fileNameFormat.toPublicString(context), - options: NoteFileNameFormat.options - .map((f) => f.toPublicString(context)) - .toList(), - onChange: (String publicStr) { - var format = - NoteFileNameFormat.fromPublicString(context, publicStr); + NoteFileNameFormatPreference( + getFileNameFormatFromConfig: () => folderConfig.fileNameFormat, + setFileNameFormatInConfig: (format) { folderConfig.fileNameFormat = format; folderConfig.save(); }, + getFileNameTemplateFromConfig: () => + folderConfig.fileNameTemplate.isEmpty + ? "{{date}} {{title}}" + : folderConfig.fileNameTemplate, + setFileNameFormatWithTemplateInConfig: (text) { + folderConfig.fileNameFormat = NoteFileNameFormat.Template; + folderConfig.fileNameTemplate = text; + folderConfig.save(); + }, ), const DefaultFileFormatTile(), const DefaultNoteFolderTile(), diff --git a/lib/settings/widgets/settings_filename_format_preference.dart b/lib/settings/widgets/settings_filename_format_preference.dart new file mode 100644 index 00000000..ad4459f4 --- /dev/null +++ b/lib/settings/widgets/settings_filename_format_preference.dart @@ -0,0 +1,238 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:gitjournal/l10n.dart'; +import 'package:gitjournal/settings/settings.dart'; +import 'package:gitjournal/settings/widgets/settings_list_preference.dart'; +import 'package:gitjournal/utils/note_filename_template.dart'; + +class NoteFileNameFormatPreference extends StatelessWidget { + final NoteFileNameFormat Function() getFileNameFormatFromConfig; + final void Function(NoteFileNameFormat format) setFileNameFormatInConfig; + final String Function() getFileNameTemplateFromConfig; + final void Function(String format) setFileNameFormatWithTemplateInConfig; + + const NoteFileNameFormatPreference({ + Key? key, + required this.getFileNameFormatFromConfig, + required this.setFileNameFormatInConfig, + required this.getFileNameTemplateFromConfig, + required this.setFileNameFormatWithTemplateInConfig, + }) : super(key: key); + + NoteFileNameFormat get configFileNameFormat => getFileNameFormatFromConfig(); + set configFileNameFormat(NoteFileNameFormat format) => + setFileNameFormatInConfig(format); + + String get configTemplate => getFileNameTemplateFromConfig(); + set configTemplate(String format) => + setFileNameFormatWithTemplateInConfig(format); + + @override + Widget build( + BuildContext context, + ) { + return ListPreference( + title: context.loc.settingsNoteNewNoteFileName, + currentOption: configFileNameFormat.toPublicString(context), + options: NoteFileNameFormat.options + .map((f) => f.toPublicString(context)) + .toList(), + onChange: (String publicStr) { + var format = NoteFileNameFormat.fromPublicString(context, publicStr); + if (format != NoteFileNameFormat.Template) { + configFileNameFormat = format; + } else { + showTemplateDialog(context); + } + }, + optionLabelBuilder: ((currentOption, option) { + if (option == NoteFileNameFormat.Template.toPublicString(context)) { + return currentOption == + NoteFileNameFormat.Template.toPublicString(context) + ? Row(children: [ + Text( + option, + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + showTemplateDialog(context); + }, + child: Text(context.loc.settingsEditorDefaultViewEdit), + ) + ]) + : Text( + option, + ); + } + + return Text( + option, + ); + }), + ); + } + + void showTemplateDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) { + return FileNameTemplatePreview( + initialTemplateText: configTemplate, + onSubmit: (value) { + configTemplate = value; + }, + ); + }, + ); + } +} + +class FileNameTemplatePreview extends StatefulWidget { + final void Function(String value) onSubmit; + final String initialTemplateText; + + @override + createState() => FileNameTemplatePreviewState(); + + const FileNameTemplatePreview({ + required this.initialTemplateText, + required this.onSubmit, + }); +} + +class FileNameTemplatePreviewState extends State { + late TextEditingController _controller; + late String _preview; + late String _errorMessage; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialTemplateText); + setPreview(_controller.text); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(context.loc.settingsNoteFileNameFormatTemplateDialogTitle), + actions: [ + TextButton( + child: Text(context.loc.settingsOk), + onPressed: _errorMessage.isEmpty + ? () { + widget.onSubmit(_controller.text); + + Navigator.of(context).pop(); + } + : null, + ), + TextButton( + child: Text(context.loc.settingsCancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + content: Column( + children: [ + const TemplateInfoWidget(), + TextField( + keyboardType: TextInputType.multiline, + maxLines: null, + controller: _controller, + onChanged: (String value) { + setState(() { + setPreview(value); + }); + }, + style: const TextStyle( + fontFamily: "monospace", + ), + decoration: InputDecoration( + label: Text( + "Template text (without file extension)", + style: TextStyle( + fontFamily: + Theme.of(context).textTheme.bodyText1!.fontFamily, + ), + ), + errorMaxLines: 10, + errorText: _errorMessage.isEmpty ? null : _errorMessage, + ), + ), + const Padding( + padding: EdgeInsets.only( + top: 10, + ), + child: Text("Preview:"), + ), + Text(_preview.isEmpty ? "--" : _preview, + style: const TextStyle( + fontFamily: "monospace", + )), + ], + )); + } + + void setPreview(String value) { + try { + final parsedTemplate = FileNameTemplate.parse(value); + final validationResult = parsedTemplate.validate(); + if (validationResult is FileNameTemplateValidationSuccess) { + _preview = parsedTemplate.render( + date: DateTime.parse("2022-12-03T10:54:42"), + title: "This Is An Example Note Title", + uuidv4: () => "12345678-1234-1234-1234-1234567890ab"); + _errorMessage = ""; + } else { + _preview = ""; + _errorMessage = + (validationResult as FileNameTemplateValidationFailure).message; + } + } catch (e) { + _preview = ""; + _errorMessage = + "Invalid template. Please check that all curly braces are closed."; + } + } +} + +class TemplateInfoWidget extends StatelessWidget { + const TemplateInfoWidget({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Expanded( + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + thickness: 10, //width of scrollbar + radius: Radius.circular(20), //corner radius of scrollbar + scrollbarOrientation: + ScrollbarOrientation.right, //which side to show scrollbar + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Text( + templateFormatHelperText, + style: TextStyle( + fontSize: 10, + fontFeatures: [FontFeature.tabularFigures()], + fontFamily: "monospace", + ), + ), + ), + ), + ); + } +} diff --git a/lib/settings/widgets/settings_list_preference.dart b/lib/settings/widgets/settings_list_preference.dart index 75eb1c8c..0d171f42 100644 --- a/lib/settings/widgets/settings_list_preference.dart +++ b/lib/settings/widgets/settings_list_preference.dart @@ -14,6 +14,10 @@ class ListPreference extends StatelessWidget { final List options; final Func1 onChange; final bool enabled; + final Widget Function(String? currentOption, String option)? + optionLabelBuilder; + final List Function(BuildContext context, String? currentOption)? + actionsBuilder; const ListPreference({ required this.title, @@ -22,6 +26,8 @@ class ListPreference extends StatelessWidget { required this.onChange, this.enabled = true, super.key, + this.optionLabelBuilder, + this.actionsBuilder, }); @override @@ -86,14 +92,16 @@ class ListPreferenceSelectionDialog extends StatelessWidget { ), ), contentPadding: EdgeInsets.zero, - actions: [ - TextButton( - child: Text(context.loc.settingsCancel), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], + actions: actionsBuilder != null + ? actionsBuilder!(context, currentOption) + : [ + TextButton( + child: Text(context.loc.settingsCancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + ], actionsPadding: const EdgeInsets.fromLTRB(0, 0, 8, 8), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), @@ -110,7 +118,7 @@ class _LabeledRadio extends StatelessWidget { required this.onChanged, }); - final String label; + final Widget label; final String? groupValue; final String? value; final Func1 onChanged; @@ -130,7 +138,7 @@ class _LabeledRadio extends StatelessWidget { value: value, onChanged: onChanged, ), - Text(label), + label, ], ), ), diff --git a/lib/utils/datetime.dart b/lib/utils/datetime.dart index 30e2dec7..89f3fce8 100644 --- a/lib/utils/datetime.dart +++ b/lib/utils/datetime.dart @@ -9,10 +9,9 @@ import 'dart:core'; import 'package:dart_git/utils/date_time.dart'; import 'package:fixnum/fixnum.dart' as fixnum; import 'package:gitjournal/core/markdown/md_yaml_note_serializer.dart'; -import 'package:intl/intl.dart'; - import 'package:gitjournal/generated/core.pb.dart' as pb; import 'package:gitjournal/logger/logger.dart'; +import 'package:intl/intl.dart'; final _dateOnlyFormat = DateFormat("yyyy-MM-dd"); final _simpleDateFormat = DateFormat("yyyy-MM-dd-HH-mm-ss"); @@ -75,14 +74,16 @@ DateTime? parseDateTime(String str) { return null; } -DateTime parseUnixTimeStamp(int val, NoteSerializationUnixTimestampMagnitude magnitude) { +DateTime parseUnixTimeStamp( + int val, NoteSerializationUnixTimestampMagnitude magnitude) { if (magnitude == NoteSerializationUnixTimestampMagnitude.Seconds) { val *= 1000; } return DateTime.fromMillisecondsSinceEpoch(val, isUtc: true); } -int toUnixTimeStamp(DateTime dt, NoteSerializationUnixTimestampMagnitude magnitude) { +int toUnixTimeStamp( + DateTime dt, NoteSerializationUnixTimestampMagnitude magnitude) { var timestamp = dt.toUtc(); switch (magnitude) { case NoteSerializationUnixTimestampMagnitude.Milliseconds: diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart index 5769ccf3..d69ee7f2 100644 --- a/lib/utils/note_filename_template.dart +++ b/lib/utils/note_filename_template.dart @@ -1,30 +1,13 @@ +import 'package:collection/collection.dart'; import 'package:gitjournal/utils/datetime.dart'; import 'package:intl/intl.dart'; - -final Map> validTemplateVariablesAndOptions = { - 'date': {'fmt'}, - 'title': { - 'lowercase', - 'uppercase', - 'snake_case', - 'kebab_case', - 'max_length', - 'default' - }, - 'uuidv4': {}, -}; - class FileNameTemplate { List<_TemplateSegment> segments; FileNameTemplate(this.segments); static FileNameTemplate parse(String template) { - try { - return FileNameTemplate(_parseTemplate(template)); - } on Exception catch (e) { - throw Exception("Problem parsing template: $e"); - } + return FileNameTemplate(_parseTemplate(template)); } FileNameTemplateValidationResult validate() { @@ -48,9 +31,12 @@ class FileNameTemplate { validTemplateVariablesAndOptions[segment.variableName] ?? {}; final invalidOptionNames = optionNames.difference(validOptionNames); if (invalidOptionNames.isNotEmpty) { - return [ - "Invalid option(s) for variable ${segment.variableName}: ${invalidOptionNames.join(', ')}" - ]; + return invalidOptionNames.length == 1 && + invalidOptionNames.firstOrNull == "" + ? ["Please specify options for variable ${segment.variableName}."] + : [ + "Invalid option(s) for variable ${segment.variableName}: ${invalidOptionNames.join(', ')}" + ]; } if (segmentsIncludeDate) { @@ -84,7 +70,6 @@ class FileNameTemplate { String render({ required DateTime date, - required String root, required String Function() uuidv4, String? title, }) { @@ -105,7 +90,7 @@ class FileNameTemplate { return renderedSegments.join(); } } - +f abstract class FileNameTemplateValidationResult { const FileNameTemplateValidationResult(); } @@ -153,7 +138,7 @@ List<_TemplateSegment> _parseTemplate(String template) { Map.fromEntries(optionsSegments.map((optionText) { final optionSegments = optionText.split('='); if (optionSegments.length > 2) { - throw Exception("Invalid option for $variableName `$optionText`"); + throw Exception("ption for $variableName `$optionText`"); } return MapEntry( optionSegments[0], @@ -186,15 +171,23 @@ class _TemplateSegment { } String _renderDate(DateTime date, Map? variableOptions) { - if (variableOptions == null || variableOptions['fmt'] == null) { - return toSimpleDateTime(date); + var result = formatDate(date, variableOptions); + + if (variableOptions != null && variableOptions['lowercase'] == 'true') { + result = result.toLowerCase(); + } else if (variableOptions != null && + variableOptions['uppercase'] == 'true') { + result = result.toUpperCase(); } - return DateFormat(variableOptions['fmt']).format(date); + return result; } String _renderTitle(String? titleInput, Map? variableOptions) { - final defaultTitle = variableOptions?['default'] ?? 'untitled'; + final defaultTitleOption = variableOptions?['default']; + final defaultTitle = defaultTitleOption == null || defaultTitleOption.isEmpty + ? 'Untitled' + : defaultTitleOption; var title = titleInput ?? defaultTitle; if (variableOptions == null) { return title; @@ -225,3 +218,111 @@ String _renderTitle(String? titleInput, Map? variableOptions) { return title; } + + +final Map> validTemplateVariablesAndOptions = { + 'date': {'lowercase', 'uppercase', 'date_only', 'hyphens', 'zettel', 'fmt'}, + 'title': { + 'lowercase', + 'uppercase', + 'snake_case', + 'kebab_case', + 'max_length', + 'default' + }, + 'uuidv4': {}, +}; + +final dateFormatOptions = {'date_only', 'hyphens', 'zettel', 'fmt'}; + +String formatDate(DateTime date, Map? options) { + if (options == null) { + return toSimpleDateTime(date); + } + final presentFormatOptions = + options.keys.toSet().difference(dateFormatOptions); + if (presentFormatOptions.length > 1) { + throw Exception( + "Only one of ${dateFormatOptions.join(', ')} can be specified"); + } + + final formatOption = presentFormatOptions.toList().firstOrNull ?? 'hyphens'; + switch (formatOption) { + case 'hyphens': + return toSimpleDateTime(date); + case 'zettel': + return toZettleDateTime(date); + case 'date_only': + return DateFormat('yyyy-MM-dd').format(date); + case 'fmt': + final fmtOptionIsBlank = (options['fmt'] == null || + options['fmt'] == 'true' || + options['fmt'] == ''); + return fmtOptionIsBlank + ? toSimpleDateTime(date) + : DateFormat(options['fmt']).format(date); + default: + throw Exception("Invalid date format option: $formatOption"); + } +} + + +const templateFormatHelperText = """ +Templates can include the following variables, +replaced with the appropriate values when the +note is created. + +Template variables are written between +double curly braces, with any options +specified after a colon. + +For example: + TEMPLATE: + {{date}}_{{title:snake_case}} + RESULT: + 2022-01-05_Title_of_the_note + +AVAILABLE TEMPLATE VARIABLES: + +{{date}} - The date and time at note creation. + OPTIONS: + hyphens - YYYY-MM-DD-HH-mm-SS + (the default format) + date_only - YYYY-MM-DD + (same as default format, but + without the time) + zettel - YYYYMMDDHHmmSS + (Zettelkasten format) + fmt - Use a custom format. The format is + specified according to the ICU/JDK + date/time pattern specification. + For example: + {{date:fmt=YY-MMMM-DD}} + produces a result like: + 22-January-05 + lowercase - Make all characters lowercase + after applying formatting. + uppercase - Make all characters uppercase + after applying formatting. +{{title}} - The title of the note. + OPTIONS: + lowercase - Make all characters lowercase. + uppercase - Make all characters uppercase. + snake_case - Convert to snake_case. + kebab_case - Convert to kebab-case. + max_length - Truncate the title to a + maximum length. + For example: + {{title:max_length=10}} + default - Set the default title if the + title is empty. + Defaults to "Untitled". + For example: + {{title:default=Untitled-note}} + {{uuidv4}} - A random UUIDv4 string. + + +Multiple options may be specified for a variable, separated commas. +For example: + {{title:lowercase,snake_case}} + """; diff --git a/test/utils/note_filename_template_test.dart b/test/utils/note_filename_template_test.dart index da49bc1e..d8ba9984 100644 --- a/test/utils/note_filename_template_test.dart +++ b/test/utils/note_filename_template_test.dart @@ -4,9 +4,14 @@ import 'package:test/test.dart'; void main() { group('valid FileNameTemplate', () { String renderTestTemplate(FileNameTemplate template, String? title) { + if (template is FileNameTemplateValidationFailure) { + throw Exception( + (template as FileNameTemplateValidationFailure).message); + } + expect(template.validate(), isA()); + return template.render( date: DateTime.parse('2022-02-27T19:00:00'), - root: 'root', uuidv4: () => 'fake_uuid', title: title, ); @@ -16,7 +21,6 @@ void main() { final template = FileNameTemplate.parse( '{{date:fmt=yyyy_MM_dd}}_{{title:lowercase,snake_case}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), '2022_02_27_some_note_title', @@ -26,10 +30,9 @@ void main() { test('title placeholder', () async { final template = FileNameTemplate.parse('{{title}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, null), - "untitled", + "Untitled", ); }); @@ -37,7 +40,6 @@ void main() { final template = FileNameTemplate.parse('{{title:default=UNTITLED_NOTE}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, null), "UNTITLED_NOTE", @@ -47,7 +49,6 @@ void main() { test('title length', () async { final template = FileNameTemplate.parse('{{title:max_length=5}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), "Some ", @@ -57,7 +58,6 @@ void main() { test('kebab case title', () async { final template = FileNameTemplate.parse('{{title:kebab_case}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title with_underscores"), "Some-note-title-with_underscores", @@ -67,7 +67,6 @@ void main() { test('snake case title', () async { final template = FileNameTemplate.parse('{{title:snake_case}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title with-hyphens"), "Some_note_title_with-hyphens", @@ -77,7 +76,6 @@ void main() { test('uppercase title', () async { final template = FileNameTemplate.parse('{{title:uppercase}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), "SOME NOTE TITLE", @@ -87,7 +85,15 @@ void main() { test('default date format', () { final template = FileNameTemplate.parse('{{date}}'); - expect(template.validate(), isA()); + expect( + renderTestTemplate(template, "Some note title"), + '2022-02-27-19-00-00', + ); + }); + + test('preset date format', () { + final template = FileNameTemplate.parse('{{date:simple}}'); + expect( renderTestTemplate(template, "Some note title"), '2022-02-27-19-00-00', @@ -97,7 +103,6 @@ void main() { test('lowercase title', () async { final template = FileNameTemplate.parse('{{title:lowercase}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), "some note title", @@ -107,7 +112,6 @@ void main() { test('custom date format', () { final template = FileNameTemplate.parse('{{date:fmt=yyyy_MM_dd}}'); - expect(template.validate(), isA()); expect( renderTestTemplate(template, "Some note title"), '2022_02_27', @@ -143,6 +147,12 @@ void main() { ); }); + test('multiple date format options', () { + final template = FileNameTemplate.parse('{{date:simple,zettel}}'); + + expect(template.validate(), isA()); + }); + test('invalid template variable', () { final template = FileNameTemplate.parse('{{invalid_template_variable}}'); From c6e437ac168eb3c2d75da7a40633ce83e6570c3d Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Fri, 16 Dec 2022 19:43:06 +0100 Subject: [PATCH 09/13] Remove f --- lib/utils/note_filename_template.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart index d69ee7f2..218a8b26 100644 --- a/lib/utils/note_filename_template.dart +++ b/lib/utils/note_filename_template.dart @@ -90,7 +90,7 @@ class FileNameTemplate { return renderedSegments.join(); } } -f + abstract class FileNameTemplateValidationResult { const FileNameTemplateValidationResult(); } From dc0ed49c77246b1e6d23c8b9429df6df3f648118 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Fri, 16 Dec 2022 20:37:47 +0100 Subject: [PATCH 10/13] Allow directories in template, disallow predictive text in template field --- lib/core/note.dart | 5 ++-- .../settings_filename_format_preference.dart | 2 ++ lib/utils/note_filename_template.dart | 24 +++++++------------ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/core/note.dart b/lib/core/note.dart index 5d4a6cfc..ef0bfa83 100644 --- a/lib/core/note.dart +++ b/lib/core/note.dart @@ -582,12 +582,11 @@ String buildTemplateFileName( String? title, String ext, ) { - // Sanitize the title - these characters are not allowed in Windows title = FileNameTemplate.parse( template, ) - .render(date: date, uuidv4: const Uuid().v4, title: title) - .replaceAll(RegExp(r'[/<\>":|?*]'), '_'); + .render(date: date, uuidv4: const Uuid().v4, title: title); + return ensureFileNameUnique(parentDir, title, ext); } diff --git a/lib/settings/widgets/settings_filename_format_preference.dart b/lib/settings/widgets/settings_filename_format_preference.dart index ad4459f4..9f932698 100644 --- a/lib/settings/widgets/settings_filename_format_preference.dart +++ b/lib/settings/widgets/settings_filename_format_preference.dart @@ -149,6 +149,8 @@ class FileNameTemplatePreviewState extends State { keyboardType: TextInputType.multiline, maxLines: null, controller: _controller, + enableSuggestions: false, + enableIMEPersonalizedLearning: false, onChanged: (String value) { setState(() { setPreview(value); diff --git a/lib/utils/note_filename_template.dart b/lib/utils/note_filename_template.dart index 218a8b26..7ff8fcaf 100644 --- a/lib/utils/note_filename_template.dart +++ b/lib/utils/note_filename_template.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:gitjournal/utils/datetime.dart'; import 'package:intl/intl.dart'; + class FileNameTemplate { List<_TemplateSegment> segments; @@ -39,16 +40,6 @@ class FileNameTemplate { ]; } - if (segmentsIncludeDate) { - final dateSegment = - segments.firstWhere((segment) => segment.variableName == 'date'); - try { - _renderDate(DateTime.now(), dateSegment.variableOptions); - } catch (e) { - return ["Invalid date format: ${dateSegment.text}"]; - } - } - if (segmentsIncludeTitle) { final titleSegment = segments.firstWhere((segment) => segment.variableName == 'title'); @@ -62,7 +53,8 @@ class FileNameTemplate { return []; }); if (segmentVariableErrors.isNotEmpty) { - return FileNameTemplateValidationFailure(segmentVariableErrors.join(';')); + return FileNameTemplateValidationFailure( + segmentVariableErrors.join('; ')); } return const FileNameTemplateValidationSuccess(); @@ -188,7 +180,11 @@ String _renderTitle(String? titleInput, Map? variableOptions) { final defaultTitle = defaultTitleOption == null || defaultTitleOption.isEmpty ? 'Untitled' : defaultTitleOption; - var title = titleInput ?? defaultTitle; + var title = (titleInput ?? defaultTitle) + // Sanitize the title - these characters are not allowed in Windows + .replaceAll(RegExp(r'[/<\>":|?*]'), '_'); + ; + if (variableOptions == null) { return title; } @@ -219,7 +215,6 @@ String _renderTitle(String? titleInput, Map? variableOptions) { return title; } - final Map> validTemplateVariablesAndOptions = { 'date': {'lowercase', 'uppercase', 'date_only', 'hyphens', 'zettel', 'fmt'}, 'title': { @@ -240,7 +235,7 @@ String formatDate(DateTime date, Map? options) { return toSimpleDateTime(date); } final presentFormatOptions = - options.keys.toSet().difference(dateFormatOptions); + options.keys.toSet().intersection(dateFormatOptions); if (presentFormatOptions.length > 1) { throw Exception( "Only one of ${dateFormatOptions.join(', ')} can be specified"); @@ -266,7 +261,6 @@ String formatDate(DateTime date, Map? options) { } } - const templateFormatHelperText = """ Templates can include the following variables, replaced with the appropriate values when the From bc5445e07c278d1746508dc5f8763e5044d689ac Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Fri, 19 May 2023 12:31:08 +0200 Subject: [PATCH 11/13] Fix rebase artifacts --- lib/settings/settings_editors.dart | 2 +- lib/settings/widgets/settings_list_preference.dart | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/settings/settings_editors.dart b/lib/settings/settings_editors.dart index 11fdad8d..fce89363 100644 --- a/lib/settings/settings_editors.dart +++ b/lib/settings/settings_editors.dart @@ -84,7 +84,7 @@ class SettingsEditorsScreenState extends State { ), ProOverlay( child: SwitchListTile( - title: Text(context.loc.featureSingleJournalEntry), + title: Text(context.loc.singleJournalEntry), value: settings.journalEditorSingleNote, onChanged: (bool newVal) { settings.journalEditorSingleNote = newVal; diff --git a/lib/settings/widgets/settings_list_preference.dart b/lib/settings/widgets/settings_list_preference.dart index 0d171f42..13b7b763 100644 --- a/lib/settings/widgets/settings_list_preference.dart +++ b/lib/settings/widgets/settings_list_preference.dart @@ -42,6 +42,8 @@ class ListPreference extends StatelessWidget { title: title, options: options, currentOption: currentOption, + actionsBuilder: actionsBuilder, + optionLabelBuilder: optionLabelBuilder, ), ); @@ -58,12 +60,18 @@ class ListPreferenceSelectionDialog extends StatelessWidget { final List options; final String? currentOption; final String title; + final List Function(BuildContext context, String? currentOption)? + actionsBuilder; + final Widget Function(String? currentOption, String option)? + optionLabelBuilder; const ListPreferenceSelectionDialog({ super.key, required this.options, this.currentOption, required this.title, + this.actionsBuilder, + this.optionLabelBuilder, }); @override @@ -80,7 +88,9 @@ class ListPreferenceSelectionDialog extends StatelessWidget { children: [ for (var o in options) _LabeledRadio( - label: o, + label: optionLabelBuilder != null + ? optionLabelBuilder!(currentOption, o) + : Text(o), value: o, groupValue: currentOption, onChanged: (String? val) { From 189999cad096bc233047e8161db89439fdc27481 Mon Sep 17 00:00:00 2001 From: justinsilvestre Date: Fri, 19 May 2023 15:12:07 +0200 Subject: [PATCH 12/13] Fix config save --- lib/core/folder/notes_folder_config.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/core/folder/notes_folder_config.dart b/lib/core/folder/notes_folder_config.dart index a7752825..e5f7e847 100644 --- a/lib/core/folder/notes_folder_config.dart +++ b/lib/core/folder/notes_folder_config.dart @@ -103,6 +103,7 @@ class NotesFolderConfig extends ChangeNotifier with SettingsSharedPref { inlineTagPrefixes = getStringSet("inlineTagPrefixes") ?? inlineTagPrefixes; + emojify = getBool("emojify") ?? emojify; allowedFileExts = getStringSet("allowedFileExts") ?? allowedFileExts; } @@ -110,10 +111,8 @@ class NotesFolderConfig extends ChangeNotifier with SettingsSharedPref { Future save() async { var def = NotesFolderConfig(id, pref); - await setString( - "noteFileNameFormat", - NoteFileNameFormat.DateOnly.toInternalString(), - NoteFileNameFormat.DateOnly.toInternalString()); + await setString("noteFileNameFormat", fileNameFormat.toInternalString(), + def.fileNameFormat.toInternalString()); await setString( "journalNoteFileNameFormat", journalFileNameFormat.toInternalString(), From 3fd8e967628cf0bccf460e70ea38defeb5c75ce0 Mon Sep 17 00:00:00 2001 From: Justin Silvestre Date: Sat, 30 Nov 2024 16:01:38 +0100 Subject: [PATCH 13/13] Fix rebase artifacts --- lib/core/note_storage.dart | 1 - lib/settings/widgets/settings_filename_format_preference.dart | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/core/note_storage.dart b/lib/core/note_storage.dart index 87a06b0d..3427ded4 100644 --- a/lib/core/note_storage.dart +++ b/lib/core/note_storage.dart @@ -13,7 +13,6 @@ import 'package:gitjournal/core/markdown/md_yaml_doc_codec.dart'; import 'package:gitjournal/core/markdown/md_yaml_doc_loader.dart'; import 'package:gitjournal/core/markdown/md_yaml_note_serializer.dart'; import 'package:gitjournal/logger/logger.dart'; -import 'package:gitjournal/utils/result.dart'; import 'package:path/path.dart' as p; import 'package:path/path.dart'; import 'package:universal_io/io.dart' as io; diff --git a/lib/settings/widgets/settings_filename_format_preference.dart b/lib/settings/widgets/settings_filename_format_preference.dart index 9f932698..ee2dceac 100644 --- a/lib/settings/widgets/settings_filename_format_preference.dart +++ b/lib/settings/widgets/settings_filename_format_preference.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:gitjournal/l10n.dart'; import 'package:gitjournal/settings/settings.dart'; @@ -164,7 +162,7 @@ class FileNameTemplatePreviewState extends State { "Template text (without file extension)", style: TextStyle( fontFamily: - Theme.of(context).textTheme.bodyText1!.fontFamily, + Theme.of(context).textTheme.bodyMedium!.fontFamily, ), ), errorMaxLines: 10,