diff --git a/.editorconfig b/.editorconfig
index b359993..41fa6e1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -18,7 +18,7 @@ indent_size = 4
tab_width = 4
# New line preferences
-insert_final_newline = false
+insert_final_newline = true
#### .NET Coding Conventions ####
[*.{cs,vb}]
diff --git a/YKanBan.Tests/Export/MarkdownExporterTests.cs b/YKanBan.Tests/Export/MarkdownExporterTests.cs
index c7f9a75..6c98aa7 100644
--- a/YKanBan.Tests/Export/MarkdownExporterTests.cs
+++ b/YKanBan.Tests/Export/MarkdownExporterTests.cs
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text;
+
using YKanBan.Export;
using YKanBan.Models;
@@ -14,8 +15,7 @@ namespace YKanBan.Tests.Export;
///
[TestClass]
[DoNotParallelize]
-public class MarkdownExporterTests
-{
+public class MarkdownExporterTests {
private static readonly DateTimeOffset ExportedAt = new(2026, 9, 30, 14, 25, 37, TimeSpan.Zero);
///
@@ -46,8 +46,7 @@ public class MarkdownExporterTests
/// Builds the board fixture: one working column with two cards, one empty column and two tags.
///
/// The columns, cards and tag/usage tuples.
- private static (IReadOnlyList Columns, IReadOnlyList Cards, IReadOnlyList<(TagModel, long)> Tags) CreateBoard()
- {
+ private static (IReadOnlyList Columns, IReadOnlyList Cards, IReadOnlyList<(TagModel, long)> Tags) CreateBoard() {
long created = UnixTimeOf(2026, 9, 1, 10, 0);
long updated = UnixTimeOf(2026, 9, 28, 18, 30);
@@ -71,8 +70,7 @@ public class MarkdownExporterTests
/// Renders the fixture board with the exporter under the current culture.
///
/// The produced Markdown document.
- private static string ExportBoard()
- {
+ private static string ExportBoard() {
(IReadOnlyList columns, IReadOnlyList cards, IReadOnlyList<(TagModel, long)> tags) = CreateBoard();
return MarkdownExporter.Export("MyRepo", columns, cards, tags, ExportedAt);
}
@@ -82,8 +80,7 @@ public class MarkdownExporterTests
///
/// The UI culture.
/// The expected Markdown document.
- private static string BuildExpected(CultureInfo culture)
- {
+ private static string BuildExpected(CultureInfo culture) {
string created = Local(2026, 9, 1, 10, 0).ToString(Resources.Export_CardTimePattern, culture);
string updated = Local(2026, 9, 28, 18, 30).ToString(Resources.Export_CardTimePattern, culture);
@@ -133,38 +130,30 @@ public class MarkdownExporterTests
/// Asserts that the exporter output matches the resource-built expected document.
///
/// The UI culture to test.
- private static void AssertOutputMatches(CultureInfo culture)
- {
+ private static void AssertOutputMatches(CultureInfo culture) {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = culture;
Assert.AreEqual(Normalize(BuildExpected(culture)), Normalize(ExportBoard()));
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
[TestMethod]
- public void EnglishOutputMatchesTemplate()
- {
+ public void EnglishOutputMatchesTemplate() {
AssertOutputMatches(new CultureInfo("en"));
}
[TestMethod]
- public void ChineseOutputMatchesTemplate()
- {
+ public void ChineseOutputMatchesTemplate() {
AssertOutputMatches(new CultureInfo("zh-Hans"));
}
[TestMethod]
- public void FileNameFollowsLanguage()
- {
+ public void FileNameFollowsLanguage() {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = new CultureInfo("en");
Assert.AreEqual(
string.Format(Resources.Culture, Resources.Export_FileNamePattern, "MyRepo", ExportedAt),
@@ -174,19 +163,15 @@ public class MarkdownExporterTests
Assert.AreEqual(
string.Format(Resources.Culture, Resources.Export_FileNamePattern, "MyRepo", ExportedAt),
MarkdownExporter.BuildFileName("MyRepo", ExportedAt));
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
[TestMethod]
- public void TagTableCellsEscapePipesAndLineBreaks()
- {
+ public void TagTableCellsEscapePipesAndLineBreaks() {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = new CultureInfo("en");
var tag = new TagModel(1, "a|b", RgbColor.Parse("#112233"), "first\nsecond");
string markdown = MarkdownExporter.Export(
@@ -197,9 +182,7 @@ public class MarkdownExporterTests
ExportedAt);
StringAssert.Contains(markdown, "| a\\|b | #112233 | first second | 0 |");
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
diff --git a/YKanBan.Tests/Launching/LaunchArgumentsTests.cs b/YKanBan.Tests/Launching/LaunchArgumentsTests.cs
index 3413b64..22c118a 100644
--- a/YKanBan.Tests/Launching/LaunchArgumentsTests.cs
+++ b/YKanBan.Tests/Launching/LaunchArgumentsTests.cs
@@ -7,32 +7,27 @@ namespace YKanBan.Tests.Launching;
/// three branches: no argument, one valid path, and invalid input.
///
[TestClass]
-public class LaunchArgumentsTests
-{
+public class LaunchArgumentsTests {
[TestMethod]
- public void NoArgumentsIsRejected()
- {
+ public void NoArgumentsIsRejected() {
Assert.IsFalse(LaunchArguments.TryParse([], out string? path));
Assert.IsNull(path);
}
[TestMethod]
- public void BlankArgumentIsRejected()
- {
+ public void BlankArgumentIsRejected() {
Assert.IsFalse(LaunchArguments.TryParse([" "], out string? path));
Assert.IsNull(path);
}
[TestMethod]
- public void MoreThanOneArgumentIsRejected()
- {
+ public void MoreThanOneArgumentIsRejected() {
Assert.IsFalse(LaunchArguments.TryParse(["a", "b"], out string? path));
Assert.IsNull(path);
}
[TestMethod]
- public void SinglePathIsAccepted()
- {
+ public void SinglePathIsAccepted() {
bool parsed = LaunchArguments.TryParse([@"C:\Repo\MyProject"], out string? path);
Assert.IsTrue(parsed);
Assert.AreEqual(@"C:\Repo\MyProject", path);
diff --git a/YKanBan.Tests/Models/RgbColorTests.cs b/YKanBan.Tests/Models/RgbColorTests.cs
index 35db145..7fc2fa6 100644
--- a/YKanBan.Tests/Models/RgbColorTests.cs
+++ b/YKanBan.Tests/Models/RgbColorTests.cs
@@ -7,11 +7,9 @@ namespace YKanBan.Tests.Models;
/// boundary in both directions.
///
[TestClass]
-public class RgbColorTests
-{
+public class RgbColorTests {
[TestMethod]
- public void ParseSplitsComponents()
- {
+ public void ParseSplitsComponents() {
RgbColor color = RgbColor.Parse("#102030");
Assert.AreEqual((byte)0x10, color.R);
Assert.AreEqual((byte)0x20, color.G);
@@ -22,8 +20,7 @@ public class RgbColorTests
[DataRow("#a1B2c3")]
[DataRow("#A1B2C3")]
[DataRow("#a1b2c3")]
- public void ParseIsCaseInsensitive(string text)
- {
+ public void ParseIsCaseInsensitive(string text) {
Assert.AreEqual(new RgbColor(0xA1, 0xB2, 0xC3), RgbColor.Parse(text));
}
@@ -36,41 +33,35 @@ public class RgbColorTests
[DataRow("#12345g")]
[DataRow(" #112233")]
[DataRow("#112233 ")]
- public void ParseRejectsMalformedText(string text)
- {
+ public void ParseRejectsMalformedText(string text) {
Assert.ThrowsExactly(() => RgbColor.Parse(text));
}
[TestMethod]
- public void TryParseRejectsNull()
- {
+ public void TryParseRejectsNull() {
Assert.IsFalse(RgbColor.TryParse(null, out RgbColor color));
Assert.AreEqual(default, color);
}
[TestMethod]
- public void TryParseAcceptsValidText()
- {
+ public void TryParseAcceptsValidText() {
Assert.IsTrue(RgbColor.TryParse("#010203", out RgbColor color));
Assert.AreEqual(new RgbColor(1, 2, 3), color);
}
[TestMethod]
- public void ToStringUsesCanonicalLowercase()
- {
+ public void ToStringUsesCanonicalLowercase() {
Assert.AreEqual("#ff0a00", new RgbColor(0xFF, 0x0A, 0x00).ToString());
}
[TestMethod]
- public void TextRoundTrips()
- {
+ public void TextRoundTrips() {
var color = new RgbColor(0x12, 0xAB, 0xCD);
Assert.AreEqual(color, RgbColor.Parse(color.ToString()));
}
[TestMethod]
- public void EqualityComparesComponents()
- {
+ public void EqualityComparesComponents() {
Assert.AreEqual(new RgbColor(1, 2, 3), new RgbColor(1, 2, 3));
Assert.AreNotEqual(new RgbColor(1, 2, 3), new RgbColor(1, 2, 4));
}
diff --git a/YKanBan.Tests/ProjectReferenceTests.cs b/YKanBan.Tests/ProjectReferenceTests.cs
index a33704f..ae569b2 100644
--- a/YKanBan.Tests/ProjectReferenceTests.cs
+++ b/YKanBan.Tests/ProjectReferenceTests.cs
@@ -7,15 +7,13 @@ namespace YKanBan.Tests;
/// application project and that the test runner can discover and execute tests.
///
[TestClass]
-public class ProjectReferenceTests
-{
+public class ProjectReferenceTests {
///
/// Verifies that a type defined in the application assembly can be
/// referenced from the test project.
///
[TestMethod]
- public void ApplicationAssemblyIsReferenceable()
- {
+ public void ApplicationAssemblyIsReferenceable() {
Assert.IsNotNull(typeof(App));
}
}
diff --git a/YKanBan.Tests/Search/SearchNodeComparer.cs b/YKanBan.Tests/Search/SearchNodeComparer.cs
index 652c3a4..8fdeb6e 100644
--- a/YKanBan.Tests/Search/SearchNodeComparer.cs
+++ b/YKanBan.Tests/Search/SearchNodeComparer.cs
@@ -7,8 +7,7 @@ namespace YKanBan.Tests.Search;
/// records compare by reference for their Terms collections by design (see the
/// YYC MARK note in SearchNodes.cs), so tests provide equality explicitly.
///
-public sealed class SearchNodeComparer : IEqualityComparer
-{
+public sealed class SearchNodeComparer : IEqualityComparer {
///
/// Gets the shared comparer instance.
///
@@ -20,19 +19,15 @@ public sealed class SearchNodeComparer : IEqualityComparer
/// The first node.
/// The second node.
/// when the nodes are structurally equal.
- public bool Equals(SearchNode? x, SearchNode? y)
- {
- if (ReferenceEquals(x, y))
- {
+ public bool Equals(SearchNode? x, SearchNode? y) {
+ if (ReferenceEquals(x, y)) {
return true;
}
- if (x is null || y is null || x.GetType() != y.GetType())
- {
+ if (x is null || y is null || x.GetType() != y.GetType()) {
return false;
}
- return (x, y) switch
- {
+ return (x, y) switch {
(SearchNode.Or a, SearchNode.Or b) => SequencesEqual(a.Terms, b.Terms),
(SearchNode.And a, SearchNode.And b) => SequencesEqual(a.Terms, b.Terms),
(SearchNode.FreeText a, SearchNode.FreeText b) => a.Text == b.Text,
@@ -58,10 +53,8 @@ public sealed class SearchNodeComparer : IEqualityComparer
/// The first sequence.
/// The second sequence.
/// when both sequences match element-wise.
- private static bool SequencesEqual(IReadOnlyList x, IReadOnlyList y)
- {
- if (x.Count != y.Count)
- {
+ private static bool SequencesEqual(IReadOnlyList x, IReadOnlyList y) {
+ if (x.Count != y.Count) {
return false;
}
return x.Zip(y).All(pair => Instance.Equals(pair.First, pair.Second));
diff --git a/YKanBan.Tests/Search/SearchQueryParserTests.cs b/YKanBan.Tests/Search/SearchQueryParserTests.cs
index d17ce04..fae748f 100644
--- a/YKanBan.Tests/Search/SearchQueryParserTests.cs
+++ b/YKanBan.Tests/Search/SearchQueryParserTests.cs
@@ -8,15 +8,13 @@ namespace YKanBan.Tests.Search;
/// ).
///
[TestClass]
-public class SearchQueryParserTests
-{
+public class SearchQueryParserTests {
///
/// Parses and asserts structural equality against the expected AST.
///
/// The expected AST.
/// The search text.
- private static void AssertParsesTo(SearchNode expected, string text)
- {
+ private static void AssertParsesTo(SearchNode expected, string text) {
SearchNode? parsed = SearchQueryParser.Parse(text);
Assert.IsNotNull(parsed, $"Blank-looking input: '{text}'.");
Assert.AreEqual(expected, parsed, SearchNodeComparer.Instance);
@@ -26,53 +24,46 @@ public class SearchQueryParserTests
/// Asserts that the input violates the grammar.
///
/// The search text.
- private static void AssertSyntaxError(string text)
- {
+ private static void AssertSyntaxError(string text) {
Assert.ThrowsExactly(() => SearchQueryParser.Parse(text));
}
#region Successful parses
[TestMethod]
- public void BlankInputReturnsNull()
- {
+ public void BlankInputReturnsNull() {
Assert.IsNull(SearchQueryParser.Parse(string.Empty));
Assert.IsNull(SearchQueryParser.Parse(" "));
}
[TestMethod]
- public void SingleWordIsFreeText()
- {
+ public void SingleWordIsFreeText() {
AssertParsesTo(new SearchNode.FreeText("login"), "login");
}
[TestMethod]
- public void AdjacentWordsAreImplicitAnd()
- {
+ public void AdjacentWordsAreImplicitAnd() {
AssertParsesTo(
new SearchNode.And([new SearchNode.FreeText("fix"), new SearchNode.FreeText("login")]),
"fix login");
}
[TestMethod]
- public void ExplicitAndKeyword()
- {
+ public void ExplicitAndKeyword() {
AssertParsesTo(
new SearchNode.And([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
"a AND b");
}
[TestMethod]
- public void OrKeyword()
- {
+ public void OrKeyword() {
AssertParsesTo(
new SearchNode.Or([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
"a OR b");
}
[TestMethod]
- public void ImplicitAndBindsTighterThanOr()
- {
+ public void ImplicitAndBindsTighterThanOr() {
AssertParsesTo(
new SearchNode.Or([
new SearchNode.And([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
@@ -81,8 +72,7 @@ public class SearchQueryParserTests
}
[TestMethod]
- public void ParenthesesForceGrouping()
- {
+ public void ParenthesesForceGrouping() {
AssertParsesTo(
new SearchNode.And([
new SearchNode.FreeText("a"),
@@ -91,16 +81,14 @@ public class SearchQueryParserTests
}
[TestMethod]
- public void NestedParenthesesCollapse()
- {
+ public void NestedParenthesesCollapse() {
AssertParsesTo(
new SearchNode.Or([new SearchNode.FreeText("a"), new SearchNode.FreeText("b")]),
"((a OR b))");
}
[TestMethod]
- public void LowercaseKeywordsArePlainWords()
- {
+ public void LowercaseKeywordsArePlainWords() {
AssertParsesTo(
new SearchNode.And([
new SearchNode.FreeText("a"),
@@ -111,14 +99,12 @@ public class SearchQueryParserTests
}
[TestMethod]
- public void PhraseIsFreeText()
- {
+ public void PhraseIsFreeText() {
AssertParsesTo(new SearchNode.FreeText("hello world"), "\"hello world\"");
}
[TestMethod]
- public void QuotedOperatorKeywordIsFreeText()
- {
+ public void QuotedOperatorKeywordIsFreeText() {
// A quoted AND/OR is literal text, not an operator.
AssertParsesTo(new SearchNode.FreeText("AND"), "\"AND\"");
AssertParsesTo(
@@ -127,54 +113,46 @@ public class SearchQueryParserTests
}
[TestMethod]
- public void PhraseSupportsEscapes()
- {
+ public void PhraseSupportsEscapes() {
// Escaped quote and escaped backslash.
AssertParsesTo(new SearchNode.FreeText("a\"b"), "\"a\\\"b\"");
AssertParsesTo(new SearchNode.FreeText("a\\b"), "\"a\\\\b\"");
}
[TestMethod]
- public void TagQualifier()
- {
+ public void TagQualifier() {
AssertParsesTo(new SearchNode.TagName("bug"), "tag:bug");
}
[TestMethod]
- public void TagQualifierAcceptsPhraseValue()
- {
+ public void TagQualifierAcceptsPhraseValue() {
AssertParsesTo(new SearchNode.TagName("a b"), "tag:\"a b\"");
}
[TestMethod]
- public void QualifierValueMayContainColons()
- {
+ public void QualifierValueMayContainColons() {
AssertParsesTo(new SearchNode.TagName("a:b"), "tag:a:b");
}
[TestMethod]
- public void TitleAndContentQualifiers()
- {
+ public void TitleAndContentQualifiers() {
AssertParsesTo(new SearchNode.FieldText(SearchField.Title, "foo"), "title:foo");
AssertParsesTo(new SearchNode.FieldText(SearchField.Content, "foo"), "content:foo");
}
[TestMethod]
- public void ColumnQualifierAcceptsPhraseValue()
- {
+ public void ColumnQualifierAcceptsPhraseValue() {
AssertParsesTo(new SearchNode.ColumnTitle("To Do"), "column:\"To Do\"");
}
[TestMethod]
- public void IdQualifierParsesNumber()
- {
+ public void IdQualifierParsesNumber() {
AssertParsesTo(new SearchNode.CardId(42), "id:42");
AssertParsesTo(new SearchNode.CardId(12), "id:\"12\"");
}
[TestMethod]
- public void ColonWithoutKeyPrefixIsFreeText()
- {
+ public void ColonWithoutKeyPrefixIsFreeText() {
AssertParsesTo(new SearchNode.FreeText(":foo"), ":foo");
}
@@ -183,106 +161,89 @@ public class SearchQueryParserTests
#region Strict failures
[TestMethod]
- public void UnterminatedPhraseIsAnError()
- {
+ public void UnterminatedPhraseIsAnError() {
AssertSyntaxError("\"abc def");
}
[TestMethod]
- public void TrailingUnterminatedPhraseIsAnError()
- {
+ public void TrailingUnterminatedPhraseIsAnError() {
AssertSyntaxError("a \"abc");
}
[TestMethod]
- public void InvalidEscapeInPhraseIsAnError()
- {
+ public void InvalidEscapeInPhraseIsAnError() {
// A bare backslash (not followed by a quote or another backslash) is invalid.
AssertSyntaxError("\"a\\b\"");
}
[TestMethod]
- public void InvalidEscapeInQualifierPhraseIsAnError()
- {
+ public void InvalidEscapeInQualifierPhraseIsAnError() {
AssertSyntaxError("tag:\"a\\x\"");
}
[TestMethod]
- public void UnbalancedOpenParenIsAnError()
- {
+ public void UnbalancedOpenParenIsAnError() {
AssertSyntaxError("(a");
}
[TestMethod]
- public void StrayCloseParenIsAnError()
- {
+ public void StrayCloseParenIsAnError() {
AssertSyntaxError("a)");
}
[TestMethod]
- public void EmptyParensAreAnError()
- {
+ public void EmptyParensAreAnError() {
AssertSyntaxError("()");
}
[TestMethod]
- public void DanglingOrIsAnError()
- {
+ public void DanglingOrIsAnError() {
AssertSyntaxError("a OR");
}
[TestMethod]
- public void DanglingAndIsAnError()
- {
+ public void DanglingAndIsAnError() {
AssertSyntaxError("a AND");
}
[TestMethod]
- public void LeadingAndIsAnError()
- {
+ public void LeadingAndIsAnError() {
AssertSyntaxError("AND a");
}
[TestMethod]
- public void OperatorAtOperandPositionIsAnError()
- {
+ public void OperatorAtOperandPositionIsAnError() {
AssertSyntaxError("a AND OR b");
}
[TestMethod]
- public void QualifierWithoutValueIsAnError()
- {
+ public void QualifierWithoutValueIsAnError() {
AssertSyntaxError("tag:");
}
[TestMethod]
- public void SpacedQualifierValueIsAnError()
- {
+ public void SpacedQualifierValueIsAnError() {
AssertSyntaxError("tag: bug");
}
[TestMethod]
- public void NonNumericIdIsAnError()
- {
+ public void NonNumericIdIsAnError() {
AssertSyntaxError("id:12x");
}
[TestMethod]
- public void UnknownQualifierKeyIsAnError()
- {
+ public void UnknownQualifierKeyIsAnError() {
AssertSyntaxError("foo:bar");
}
[TestMethod]
- public void QualifierKeysAreCaseSensitive()
- {
+ public void QualifierKeysAreCaseSensitive() {
// "Tag" is a key-like prefix but not the known lowercase "tag", so it is unknown.
AssertSyntaxError("Tag:x");
}
[TestMethod]
- public void SyntaxErrorCarriesDescriptiveMessage()
- {
+ public void SyntaxErrorCarriesDescriptiveMessage() {
SearchSyntaxException exception = Assert.ThrowsExactly(
() => SearchQueryParser.Parse("a AND"));
Assert.IsFalse(string.IsNullOrEmpty(exception.Message));
diff --git a/YKanBan.Tests/Search/SearchSqlCompilerTests.cs b/YKanBan.Tests/Search/SearchSqlCompilerTests.cs
index 836b4f5..7c8fc04 100644
--- a/YKanBan.Tests/Search/SearchSqlCompilerTests.cs
+++ b/YKanBan.Tests/Search/SearchSqlCompilerTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+
using YKanBan.Search;
using YKanBan.Storage.Workspace;
using YKanBan.Tests.TestUtilities;
@@ -10,22 +11,18 @@ namespace YKanBan.Tests.Search;
/// workspace database.
///
[TestClass]
-public class SearchSqlCompilerTests
-{
+public class SearchSqlCompilerTests {
///
/// Owns a workspace with a fixed card/tag fixture.
///
- private sealed class SearchFixture : IDisposable
- {
+ private sealed class SearchFixture : IDisposable {
public TempDirectory Directory { get; } = new();
public SqliteConnection Connection { get; }
- public SearchFixture()
- {
+ public SearchFixture() {
WorkspaceInitializer.Initialize(Directory.FullPath);
- using (SqliteConnection setup = SqliteTestHelper.OpenWorkspace(Directory.FullPath))
- {
+ using (SqliteConnection setup = SqliteTestHelper.OpenWorkspace(Directory.FullPath)) {
// Columns: 1 "To Do", 2 "In Progress".
SqliteTestHelper.Exec(setup, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('To Do', '', 1, 1);");
SqliteTestHelper.Exec(setup, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('In Progress', '', 1, 1);");
@@ -54,38 +51,33 @@ public class SearchSqlCompilerTests
///
/// The search text.
/// The matching card ids in id order.
- public List Search(string queryText)
- {
+ public List Search(string queryText) {
SearchNode? query = SearchQueryParser.Parse(queryText);
Assert.IsNotNull(query, "Blank fixture queries are not expected here.");
CompiledSearch compiled = SearchSqlCompiler.Compile(query);
using var command = Connection.CreateCommand();
command.CommandText = $"SELECT c.id FROM cards c WHERE {compiled.Predicate} ORDER BY c.id ASC;";
- foreach ((string name, object value) in compiled.Parameters)
- {
+ foreach ((string name, object value) in compiled.Parameters) {
command.Parameters.AddWithValue(name, value);
}
using var reader = command.ExecuteReader();
var ids = new List();
- while (reader.Read())
- {
+ while (reader.Read()) {
ids.Add(reader.GetInt64(0));
}
return ids;
}
- public void Dispose()
- {
+ public void Dispose() {
Connection.Dispose();
Directory.Dispose();
}
}
[TestMethod]
- public void FreeTextMatchesTitleOrContentCaseInsensitively()
- {
+ public void FreeTextMatchesTitleOrContentCaseInsensitively() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 2L }, fixture.Search("apple"));
CollectionAssert.AreEqual(new[] { 1L }, fixture.Search("login"));
@@ -94,8 +86,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
- public void FieldQualifiersScopeToTheirField()
- {
+ public void FieldQualifiersScopeToTheirField() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 2L }, fixture.Search("title:apple"));
CollectionAssert.AreEqual(new[] { 3L }, fixture.Search("content:banana"));
@@ -103,8 +94,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
- public void TagQualifierMatchesExactlyAndCaseInsensitively()
- {
+ public void TagQualifierMatchesExactlyAndCaseInsensitively() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 1L }, fixture.Search("tag:bug"));
CollectionAssert.AreEqual(new[] { 2L, 3L }, fixture.Search("tag:ui"));
@@ -116,8 +106,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
- public void ColumnQualifierMatchesColumnTitleExactly()
- {
+ public void ColumnQualifierMatchesColumnTitleExactly() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 1L, 4L }, fixture.Search("column:\"to do\""));
CollectionAssert.AreEqual(new[] { 2L, 3L }, fixture.Search("column:\"in progress\""));
@@ -128,16 +117,14 @@ public class SearchSqlCompilerTests
}
[TestMethod]
- public void IdQualifierMatchesExactly()
- {
+ public void IdQualifierMatchesExactly() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 3L }, fixture.Search("id:3"));
Assert.AreEqual(0, fixture.Search("id:99").Count);
}
[TestMethod]
- public void AndOrCombination()
- {
+ public void AndOrCombination() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 2L, 3L }, fixture.Search("apple OR banana"));
CollectionAssert.AreEqual(new[] { 2L }, fixture.Search("apple AND id:2"));
@@ -145,8 +132,7 @@ public class SearchSqlCompilerTests
}
[TestMethod]
- public void ParenthesizedGrouping()
- {
+ public void ParenthesizedGrouping() {
using var fixture = new SearchFixture();
// (apple OR banana) hits cards 2,3; intersected with column "In Progress" (2,3).
@@ -157,16 +143,14 @@ public class SearchSqlCompilerTests
}
[TestMethod]
- public void LikeWildcardsInUserTextMatchLiterally()
- {
+ public void LikeWildcardsInUserTextMatchLiterally() {
using var fixture = new SearchFixture();
CollectionAssert.AreEqual(new[] { 4L }, fixture.Search("100%"));
CollectionAssert.AreEqual(new[] { 4L }, fixture.Search("a_b"));
}
[TestMethod]
- public void CompiledPredicateUsesNamedParameters()
- {
+ public void CompiledPredicateUsesNamedParameters() {
CompiledSearch compiled = SearchSqlCompiler.Compile(new SearchNode.FreeText("x"));
StringAssert.Contains(compiled.Predicate, "$p0");
diff --git a/YKanBan.Tests/Storage/AppData/AppConfigStoreTests.cs b/YKanBan.Tests/Storage/AppData/AppConfigStoreTests.cs
index fe45e21..c1a3b1f 100644
--- a/YKanBan.Tests/Storage/AppData/AppConfigStoreTests.cs
+++ b/YKanBan.Tests/Storage/AppData/AppConfigStoreTests.cs
@@ -8,11 +8,9 @@ namespace YKanBan.Tests.Storage.AppData;
/// degradation on a missing or corrupt file.
///
[TestClass]
-public class AppConfigStoreTests
-{
+public class AppConfigStoreTests {
[TestMethod]
- public void LoadMissingFileReturnsDefaults()
- {
+ public void LoadMissingFileReturnsDefaults() {
using var directory = new TempDirectory();
var store = new AppConfigStore(Path.Combine(directory.FullPath, "app.json"));
@@ -32,8 +30,7 @@ public class AppConfigStoreTests
}
[TestMethod]
- public void LoadCorruptFileBacksUpAndReturnsDefaults()
- {
+ public void LoadCorruptFileBacksUpAndReturnsDefaults() {
using var directory = new TempDirectory();
string path = Path.Combine(directory.FullPath, "app.json");
File.WriteAllText(path, "{ this is not valid json");
@@ -47,8 +44,7 @@ public class AppConfigStoreTests
}
[TestMethod]
- public void LoadJsonNullReturnsDefaults()
- {
+ public void LoadJsonNullReturnsDefaults() {
using var directory = new TempDirectory();
string path = Path.Combine(directory.FullPath, "app.json");
File.WriteAllText(path, "null");
@@ -58,8 +54,7 @@ public class AppConfigStoreTests
}
[TestMethod]
- public void LoadReadsKebabCaseKeysAndEnumValues()
- {
+ public void LoadReadsKebabCaseKeysAndEnumValues() {
using var directory = new TempDirectory();
string path = Path.Combine(directory.FullPath, "app.json");
File.WriteAllText(path, $$"""
@@ -97,8 +92,7 @@ public class AppConfigStoreTests
}
[TestMethod]
- public void LoadMissingSectionsFillsDefaults()
- {
+ public void LoadMissingSectionsFillsDefaults() {
using var directory = new TempDirectory();
string path = Path.Combine(directory.FullPath, "app.json");
File.WriteAllText(path, $$"""{ "version": {{AppConfig.CurrentFormatVersion}} }""");
@@ -113,17 +107,14 @@ public class AppConfigStoreTests
}
[TestMethod]
- public void SaveWritesKebabCaseKeysAndEnumValues()
- {
+ public void SaveWritesKebabCaseKeysAndEnumValues() {
using var directory = new TempDirectory();
string path = Path.Combine(directory.FullPath, "app.json");
var store = new AppConfigStore(path);
- var config = new AppConfig
- {
+ var config = new AppConfig {
Theme = ThemeOption.FollowSystem,
- Display = new DisplaySettings
- {
+ Display = new DisplaySettings {
CardSort = SortOption.UpdatedDescending,
ColumnSort = SortOption.CreatedAscending,
ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Custom, CustomPixels = 500 },
@@ -147,18 +138,15 @@ public class AppConfigStoreTests
}
[TestMethod]
- public void SaveThenLoadRoundTrips()
- {
+ public void SaveThenLoadRoundTrips() {
using var directory = new TempDirectory();
string path = Path.Combine(directory.FullPath, "app.json");
var store = new AppConfigStore(path);
- var config = new AppConfig
- {
+ var config = new AppConfig {
Language = "zh-Hans",
Theme = ThemeOption.Light,
- Display = new DisplaySettings
- {
+ Display = new DisplaySettings {
CardSort = SortOption.CreatedDescending,
ColumnSort = SortOption.TitleAscending,
ColumnWidth = new ColumnWidthSettings { Preset = ColumnWidthPreset.Wide },
diff --git a/YKanBan.Tests/Storage/SqliteDatabaseTests.cs b/YKanBan.Tests/Storage/SqliteDatabaseTests.cs
index 1bf3be8..9c1fd5b 100644
--- a/YKanBan.Tests/Storage/SqliteDatabaseTests.cs
+++ b/YKanBan.Tests/Storage/SqliteDatabaseTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+
using YKanBan.Storage;
using YKanBan.Storage.Workspace;
using YKanBan.Tests.TestUtilities;
@@ -10,11 +11,9 @@ namespace YKanBan.Tests.Storage;
/// user_version-driven migration runner.
///
[TestClass]
-public class SqliteDatabaseTests
-{
+public class SqliteDatabaseTests {
[TestMethod]
- public void OpenAppliesMandatoryPragmas()
- {
+ public void OpenAppliesMandatoryPragmas() {
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "pragmas.db");
@@ -25,8 +24,7 @@ public class SqliteDatabaseTests
}
[TestMethod]
- public void OpenSetsUserVersionToLatestMigration()
- {
+ public void OpenSetsUserVersionToLatestMigration() {
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "version.db");
@@ -35,13 +33,11 @@ public class SqliteDatabaseTests
}
[TestMethod]
- public void ReopenKeepsWalJournalMode()
- {
+ public void ReopenKeepsWalJournalMode() {
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "wal.db");
- using (SqliteConnection connection = SqliteDatabase.Open(databasePath, WorkspaceSchema.Migrations))
- {
+ using (SqliteConnection connection = SqliteDatabase.Open(databasePath, WorkspaceSchema.Migrations)) {
}
// WAL mode is stored in the database header, so a fresh connection keeps it.
@@ -50,14 +46,12 @@ public class SqliteDatabaseTests
}
[TestMethod]
- public void OpenRejectsNewerStoredSchemaVersion()
- {
+ public void OpenRejectsNewerStoredSchemaVersion() {
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "newer.db");
long newerVersion = WorkspaceSchema.CurrentVersion + 1;
- using (SqliteConnection connection = SqliteDatabase.Open(databasePath, WorkspaceSchema.Migrations))
- {
+ using (SqliteConnection connection = SqliteDatabase.Open(databasePath, WorkspaceSchema.Migrations)) {
SqliteTestHelper.Exec(connection, $"PRAGMA user_version={newerVersion};");
}
@@ -68,8 +62,7 @@ public class SqliteDatabaseTests
}
[TestMethod]
- public void OpenRejectsNonContiguousMigrationList()
- {
+ public void OpenRejectsNonContiguousMigrationList() {
using var directory = new TempDirectory();
string databasePath = Path.Combine(directory.FullPath, "gap.db");
diff --git a/YKanBan.Tests/Storage/Workspace/WorkspaceInitializerTests.cs b/YKanBan.Tests/Storage/Workspace/WorkspaceInitializerTests.cs
index 515cd7b..b2e45ff 100644
--- a/YKanBan.Tests/Storage/Workspace/WorkspaceInitializerTests.cs
+++ b/YKanBan.Tests/Storage/Workspace/WorkspaceInitializerTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+
using YKanBan.Storage;
using YKanBan.Storage.Workspace;
using YKanBan.Tests.TestUtilities;
@@ -11,11 +12,9 @@ namespace YKanBan.Tests.Storage.Workspace;
/// content.
///
[TestClass]
-public class WorkspaceInitializerTests
-{
+public class WorkspaceInitializerTests {
[TestMethod]
- public void IsWorkspaceReflectsInitializationState()
- {
+ public void IsWorkspaceReflectsInitializationState() {
using var directory = new TempDirectory();
Assert.IsFalse(WorkspaceInitializer.IsWorkspace(directory.FullPath));
@@ -24,8 +23,7 @@ public class WorkspaceInitializerTests
}
[TestMethod]
- public void InitializeCreatesStructureAndMigratedEmptyDatabase()
- {
+ public void InitializeCreatesStructureAndMigratedEmptyDatabase() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
@@ -43,8 +41,7 @@ public class WorkspaceInitializerTests
}
[TestMethod]
- public void InitializeMissingFolderThrows()
- {
+ public void InitializeMissingFolderThrows() {
using var directory = new TempDirectory();
string missing = Path.Combine(directory.FullPath, "not-created");
@@ -54,8 +51,7 @@ public class WorkspaceInitializerTests
}
[TestMethod]
- public void InitializeTwiceThrows()
- {
+ public void InitializeTwiceThrows() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
diff --git a/YKanBan.Tests/Storage/Workspace/WorkspaceLockTests.cs b/YKanBan.Tests/Storage/Workspace/WorkspaceLockTests.cs
index 04d4b41..e804b60 100644
--- a/YKanBan.Tests/Storage/Workspace/WorkspaceLockTests.cs
+++ b/YKanBan.Tests/Storage/Workspace/WorkspaceLockTests.cs
@@ -8,11 +8,9 @@ namespace YKanBan.Tests.Storage.Workspace;
/// Tests for exclusive lock acquisition and release.
///
[TestClass]
-public class WorkspaceLockTests
-{
+public class WorkspaceLockTests {
[TestMethod]
- public void AcquireThenDisposeThenAcquireAgainSucceeds()
- {
+ public void AcquireThenDisposeThenAcquireAgainSucceeds() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
@@ -22,8 +20,7 @@ public class WorkspaceLockTests
}
[TestMethod]
- public void SecondAcquireWhileHeldThrows()
- {
+ public void SecondAcquireWhileHeldThrows() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
@@ -34,21 +31,18 @@ public class WorkspaceLockTests
}
[TestMethod]
- public void AcquireWithoutYkanbanThrowsNotInitialized()
- {
+ public void AcquireWithoutYkanbanThrowsNotInitialized() {
using var directory = new TempDirectory();
Assert.ThrowsExactly(
() => WorkspaceLock.Acquire(directory.FullPath));
}
[TestMethod]
- public void LockFileIsEmptyAfterRelease()
- {
+ public void LockFileIsEmptyAfterRelease() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
- using (WorkspaceLock.Acquire(directory.FullPath))
- {
+ using (WorkspaceLock.Acquire(directory.FullPath)) {
}
// The lock file survives release but carries no content; only the OS handle matters.
diff --git a/YKanBan.Tests/Storage/Workspace/WorkspacePresetTests.cs b/YKanBan.Tests/Storage/Workspace/WorkspacePresetTests.cs
index eff247b..9c7a192 100644
--- a/YKanBan.Tests/Storage/Workspace/WorkspacePresetTests.cs
+++ b/YKanBan.Tests/Storage/Workspace/WorkspacePresetTests.cs
@@ -1,5 +1,7 @@
using System.Globalization;
+
using Microsoft.Data.Sqlite;
+
using YKanBan.Storage.Workspace;
using YKanBan.Tests.TestUtilities;
@@ -12,14 +14,11 @@ namespace YKanBan.Tests.Storage.Workspace;
///
[TestClass]
[DoNotParallelize]
-public class WorkspacePresetTests
-{
+public class WorkspacePresetTests {
[TestMethod]
- public void AddPresetColumnsStoresEnglishTextUnderEnglishCulture()
- {
+ public void AddPresetColumnsStoresEnglishTextUnderEnglishCulture() {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = new CultureInfo("en");
using var directory = new TempDirectory();
@@ -43,19 +42,15 @@ public class WorkspacePresetTests
Resources.PresetColumn_Done_Description,
},
SqliteTestHelper.GetColumnDescriptions(connection));
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
[TestMethod]
- public void AddPresetColumnsStoresChineseTextUnderChineseCulture()
- {
+ public void AddPresetColumnsStoresChineseTextUnderChineseCulture() {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = new CultureInfo("zh-Hans");
using var directory = new TempDirectory();
@@ -74,19 +69,15 @@ public class WorkspacePresetTests
// Chinese resources must resolve to different text than English.
Assert.AreEqual("待办", Resources.PresetColumn_Todo);
Assert.AreEqual("已完成的任务。", Resources.PresetColumn_Done_Description);
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
[TestMethod]
- public void AddPresetTagsStoresEightTagsUnderEnglishCulture()
- {
+ public void AddPresetTagsStoresEightTagsUnderEnglishCulture() {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = new CultureInfo("en");
using var directory = new TempDirectory();
@@ -100,19 +91,15 @@ public class WorkspacePresetTests
Assert.AreEqual(("bug", "#d73a4a", "Something isn't working"), tags[0]);
Assert.AreEqual(("documentation", "#0075ca", "Improvements or additions to documentation"), tags[1]);
Assert.AreEqual(("wontfix", "#ffffff", "This will not be worked on"), tags[7]);
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
[TestMethod]
- public void AddPresetTagsStoresChineseNamesUnderChineseCulture()
- {
+ public void AddPresetTagsStoresChineseNamesUnderChineseCulture() {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = new CultureInfo("zh-Hans");
using var directory = new TempDirectory();
@@ -125,19 +112,15 @@ public class WorkspacePresetTests
Assert.AreEqual(8, tags.Count);
Assert.AreEqual("缺陷", tags[0].Name);
Assert.AreEqual("功能没有正常工作", tags[0].Description);
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
[TestMethod]
- public void PresetContentIsDataAndIgnoresLaterCultureChanges()
- {
+ public void PresetContentIsDataAndIgnoresLaterCultureChanges() {
CultureInfo? original = Resources.Culture;
- try
- {
+ try {
Resources.Culture = new CultureInfo("en");
using var directory = new TempDirectory();
@@ -150,9 +133,7 @@ public class WorkspacePresetTests
using SqliteConnection connection = SqliteTestHelper.OpenWorkspace(directory.FullPath);
Assert.AreEqual("To Do", SqliteTestHelper.GetColumnTitles(connection)[0]);
Assert.AreEqual("bug", SqliteTestHelper.GetTags(connection)[0].Name);
- }
- finally
- {
+ } finally {
Resources.Culture = original;
}
}
diff --git a/YKanBan.Tests/Storage/Workspace/WorkspaceRepositoryTests.cs b/YKanBan.Tests/Storage/Workspace/WorkspaceRepositoryTests.cs
index d5d4bd4..fc89a6c 100644
--- a/YKanBan.Tests/Storage/Workspace/WorkspaceRepositoryTests.cs
+++ b/YKanBan.Tests/Storage/Workspace/WorkspaceRepositoryTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+
using YKanBan.Models;
using YKanBan.Search;
using YKanBan.Storage.Workspace;
@@ -11,16 +12,14 @@ namespace YKanBan.Tests.Storage.Workspace;
/// transaction atomicity and the timestamp refresh rules.
///
[TestClass]
-public class WorkspaceRepositoryTests
-{
+public class WorkspaceRepositoryTests {
///
/// Owns a workspace with fixed-timestamp fixture data (one column, one
/// card, two tags linked to the card) plus the repository under test.
/// Fixture timestamps are small literal values so that any real write,
/// which stamps the current Unix time, is observable.
///
- private sealed class RepositoryFixture : IDisposable
- {
+ private sealed class RepositoryFixture : IDisposable {
public TempDirectory Directory { get; } = new();
public WorkspaceRepository Repository { get; }
@@ -33,12 +32,10 @@ public class WorkspaceRepositoryTests
public long UiTagId { get; } = 2;
- public RepositoryFixture()
- {
+ public RepositoryFixture() {
WorkspaceInitializer.Initialize(Directory.FullPath);
- using (SqliteConnection connection = SqliteTestHelper.OpenWorkspace(Directory.FullPath))
- {
+ using (SqliteConnection connection = SqliteTestHelper.OpenWorkspace(Directory.FullPath)) {
SqliteTestHelper.Exec(connection, "INSERT INTO columns (title, description, created_at, updated_at) VALUES ('Col', '', 100, 100);");
SqliteTestHelper.Exec(connection, "INSERT INTO cards (column_id, title, content, created_at, updated_at) VALUES (1, 'T', 'C', 100, 100);");
SqliteTestHelper.Exec(connection, "INSERT INTO tags (name, color, description) VALUES ('bug', '#ff0000', '');");
@@ -50,16 +47,14 @@ public class WorkspaceRepositoryTests
Repository = new WorkspaceRepository(Directory.FullPath);
}
- public void Dispose()
- {
+ public void Dispose() {
Repository.Dispose();
Directory.Dispose();
}
}
[TestMethod]
- public void GetColumnsReturnsTheSeededColumn()
- {
+ public void GetColumnsReturnsTheSeededColumn() {
using var fixture = new RepositoryFixture();
ColumnModel column = fixture.Repository.GetColumn(fixture.ColumnId)!;
Assert.AreEqual("Col", column.Title);
@@ -67,8 +62,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void AddColumnSetsEqualTimestamps()
- {
+ public void AddColumnSetsEqualTimestamps() {
using var fixture = new RepositoryFixture();
ColumnModel column = fixture.Repository.AddColumn("New", "desc");
Assert.IsTrue(column.CreatedAt > 0L);
@@ -76,8 +70,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateColumnWithoutChangeWritesNothing()
- {
+ public void UpdateColumnWithoutChangeWritesNothing() {
using var fixture = new RepositoryFixture();
ColumnModel result = fixture.Repository.UpdateColumn(fixture.ColumnId, "Col", "");
@@ -87,8 +80,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateColumnChangeRefreshesTimestamp()
- {
+ public void UpdateColumnChangeRefreshesTimestamp() {
using var fixture = new RepositoryFixture();
ColumnModel updated = fixture.Repository.UpdateColumn(fixture.ColumnId, "Col2", "desc");
@@ -98,8 +90,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void DeleteColumnCascadesToCardsAndTagLinks()
- {
+ public void DeleteColumnCascadesToCardsAndTagLinks() {
using var fixture = new RepositoryFixture();
Assert.IsTrue(fixture.Repository.DeleteColumn(fixture.ColumnId));
@@ -112,8 +103,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void DeleteCardRemovesTagLinksButKeepsTags()
- {
+ public void DeleteCardRemovesTagLinksButKeepsTags() {
using var fixture = new RepositoryFixture();
Assert.IsTrue(fixture.Repository.DeleteCard(fixture.CardId));
@@ -125,8 +115,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void DeleteTagDetachesFromCardsButKeepsCards()
- {
+ public void DeleteTagDetachesFromCardsButKeepsCards() {
using var fixture = new RepositoryFixture();
Assert.IsTrue(fixture.Repository.DeleteTag(fixture.BugTagId));
@@ -138,8 +127,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateCardWithoutEffectiveChangeWritesNothing()
- {
+ public void UpdateCardWithoutEffectiveChangeWritesNothing() {
using var fixture = new RepositoryFixture();
CardModel result = fixture.Repository.UpdateCard(
@@ -149,8 +137,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateCardTitleChangeRefreshesTimestamp()
- {
+ public void UpdateCardTitleChangeRefreshesTimestamp() {
using var fixture = new RepositoryFixture();
CardModel updated = fixture.Repository.UpdateCard(
@@ -161,8 +148,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateCardTagSetOnlyChangeStillRefreshesTimestamp()
- {
+ public void UpdateCardTagSetOnlyChangeStillRefreshesTimestamp() {
using var fixture = new RepositoryFixture();
CardModel updated = fixture.Repository.UpdateCard(fixture.CardId, "T", "C", [fixture.BugTagId]);
@@ -172,8 +158,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateCardWithUnknownTagRollsBackAtomically()
- {
+ public void UpdateCardWithUnknownTagRollsBackAtomically() {
using var fixture = new RepositoryFixture();
// Tag 999 does not exist: the FK fires mid-transaction and the whole edit must roll back.
@@ -188,8 +173,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void MoveCardUpdatesColumnWithoutRefreshingTimestamp()
- {
+ public void MoveCardUpdatesColumnWithoutRefreshingTimestamp() {
using var fixture = new RepositoryFixture();
long target = fixture.Repository.AddColumn("Target", "").Id;
@@ -202,8 +186,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void MoveCardToCurrentColumnIsNoOp()
- {
+ public void MoveCardToCurrentColumnIsNoOp() {
using var fixture = new RepositoryFixture();
Assert.IsFalse(fixture.Repository.MoveCard(fixture.CardId, fixture.ColumnId));
@@ -212,8 +195,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void AddCardCreatesWithEqualTimestampsAndLinkedTags()
- {
+ public void AddCardCreatesWithEqualTimestampsAndLinkedTags() {
using var fixture = new RepositoryFixture();
CardModel card = fixture.Repository.AddCard(fixture.ColumnId, "N", "body", [fixture.UiTagId]);
@@ -225,8 +207,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateTagWithoutChangeWritesNothing()
- {
+ public void UpdateTagWithoutChangeWritesNothing() {
using var fixture = new RepositoryFixture();
TagModel result = fixture.Repository.UpdateTag(
@@ -237,8 +218,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void UpdateTagChangeWrites()
- {
+ public void UpdateTagChangeWrites() {
using var fixture = new RepositoryFixture();
TagModel updated = fixture.Repository.UpdateTag(
@@ -251,16 +231,14 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void AddTagWithDuplicateNameIsRejected()
- {
+ public void AddTagWithDuplicateNameIsRejected() {
using var fixture = new RepositoryFixture();
Assert.ThrowsExactly(() =>
fixture.Repository.AddTag("bug", RgbColor.Parse("#123456"), ""));
}
[TestMethod]
- public void GetCardIdsMatchingRunsTheCompiledSearch()
- {
+ public void GetCardIdsMatchingRunsTheCompiledSearch() {
using var fixture = new RepositoryFixture();
CompiledSearch compiled = SearchSqlCompiler.Compile(new SearchNode.FreeText("T"));
@@ -270,8 +248,7 @@ public class WorkspaceRepositoryTests
}
[TestMethod]
- public void GetTagsWithUsageCountsAssignments()
- {
+ public void GetTagsWithUsageCountsAssignments() {
using var fixture = new RepositoryFixture();
IReadOnlyList<(TagModel Tag, long UsageCount)> before = fixture.Repository.GetTagsWithUsage();
diff --git a/YKanBan.Tests/Storage/Workspace/WorkspaceSchemaTests.cs b/YKanBan.Tests/Storage/Workspace/WorkspaceSchemaTests.cs
index 7e2d5b1..43bbbb1 100644
--- a/YKanBan.Tests/Storage/Workspace/WorkspaceSchemaTests.cs
+++ b/YKanBan.Tests/Storage/Workspace/WorkspaceSchemaTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+
using YKanBan.Storage;
using YKanBan.Storage.Workspace;
using YKanBan.Tests.TestUtilities;
@@ -10,19 +11,16 @@ namespace YKanBan.Tests.Storage.Workspace;
/// and that the cascading delete chains work as specified.
///
[TestClass]
-public class WorkspaceSchemaTests
-{
+public class WorkspaceSchemaTests {
///
/// Owns a temp directory plus a fresh workspace-schema database for a single test.
///
- private sealed class FreshDatabase : IDisposable
- {
+ private sealed class FreshDatabase : IDisposable {
public TempDirectory Directory { get; } = new();
public SqliteConnection Connection { get; }
- public FreshDatabase()
- {
+ public FreshDatabase() {
Connection = SqliteDatabase.Open(
Path.Combine(Directory.FullPath, "ws.db"), WorkspaceSchema.Migrations);
}
@@ -30,24 +28,21 @@ public class WorkspaceSchemaTests
///
/// Closes the connection before removing the temporary directory.
///
- public void Dispose()
- {
+ public void Dispose() {
Connection.Dispose();
Directory.Dispose();
}
}
[TestMethod]
- public void EmptyColumnTitleIsRejected()
- {
+ public void EmptyColumnTitleIsRejected() {
using var db = new FreshDatabase();
Assert.ThrowsExactly(() =>
SqliteTestHelper.Exec(db.Connection, "INSERT INTO columns (title, created_at, updated_at) VALUES ('', 1, 1);"));
}
[TestMethod]
- public void WhitespaceColumnTitleIsAccepted()
- {
+ public void WhitespaceColumnTitleIsAccepted() {
using var db = new FreshDatabase();
// No trimming: a single space is a non-empty title as far as the DDL is concerned.
int affected = SqliteTestHelper.Exec(
@@ -56,8 +51,7 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void DuplicateColumnTitleIsRejected()
- {
+ public void DuplicateColumnTitleIsRejected() {
using var db = new FreshDatabase();
SqliteTestHelper.Exec(db.Connection, "INSERT INTO columns (title, created_at, updated_at) VALUES ('Foo', 1, 1);");
Assert.ThrowsExactly(() =>
@@ -65,8 +59,7 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void ColumnTitlesCompareCaseSensitively()
- {
+ public void ColumnTitlesCompareCaseSensitively() {
using var db = new FreshDatabase();
// BINARY collation: 'Foo' and 'foo' are distinct titles.
SqliteTestHelper.Exec(db.Connection, "INSERT INTO columns (title, created_at, updated_at) VALUES ('Foo', 1, 1);");
@@ -75,28 +68,24 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void EmptyTagNameIsRejected()
- {
+ public void EmptyTagNameIsRejected() {
using var db = new FreshDatabase();
Assert.ThrowsExactly(() =>
SqliteTestHelper.Exec(db.Connection, "INSERT INTO tags (name, color) VALUES ('', '#112233');"));
}
[TestMethod]
- public void TagNamesAcceptSpacesEmojiAndReservedWords()
- {
+ public void TagNamesAcceptSpacesEmojiAndReservedWords() {
using var db = new FreshDatabase();
// Tag names are fully open: no charset or length rules, no trimming.
- foreach (string name in new[] { " ", "a b", "🏷️", "AND", "tag:with:colons" })
- {
+ foreach (string name in new[] { " ", "a b", "🏷️", "AND", "tag:with:colons" }) {
SqliteTestHelper.Exec(db.Connection, "INSERT INTO tags (name, color) VALUES ($n, '#112233');", ("$n", name));
}
Assert.AreEqual(5L, SqliteTestHelper.ScalarLong(db.Connection, "SELECT COUNT(*) FROM tags;"));
}
[TestMethod]
- public void DuplicateTagNameIsRejected()
- {
+ public void DuplicateTagNameIsRejected() {
using var db = new FreshDatabase();
SqliteTestHelper.Exec(db.Connection, "INSERT INTO tags (name, color) VALUES ('bug', '#112233');");
Assert.ThrowsExactly(() =>
@@ -107,8 +96,7 @@ public class WorkspaceSchemaTests
[DataRow("#000000")]
[DataRow("#FFFFFF")]
[DataRow("#a1B2c3")]
- public void ValidTagColorsAreAccepted(string color)
- {
+ public void ValidTagColorsAreAccepted(string color) {
using var db = new FreshDatabase();
int affected = SqliteTestHelper.Exec(
db.Connection, "INSERT INTO tags (name, color) VALUES ('x', $c);", ("$c", color));
@@ -124,16 +112,14 @@ public class WorkspaceSchemaTests
[DataRow(" #112233")]
[DataRow("#112233 ")]
[DataRow("")]
- public void InvalidTagColorsAreRejected(string color)
- {
+ public void InvalidTagColorsAreRejected(string color) {
using var db = new FreshDatabase();
Assert.ThrowsExactly(() =>
SqliteTestHelper.Exec(db.Connection, "INSERT INTO tags (name, color) VALUES ('x', $c);", ("$c", color)));
}
[TestMethod]
- public void CardRequiresExistingColumn()
- {
+ public void CardRequiresExistingColumn() {
using var db = new FreshDatabase();
// foreign_keys=ON makes the FK enforceable; this is the DDL contract behind the cascades.
Assert.ThrowsExactly(() =>
@@ -141,8 +127,7 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void CardDefaultsToEmptyTitleAndContent()
- {
+ public void CardDefaultsToEmptyTitleAndContent() {
using var db = new FreshDatabase();
SqliteTestHelper.Exec(db.Connection, "INSERT INTO columns (title, created_at, updated_at) VALUES ('c', 1, 1);");
SqliteTestHelper.Exec(db.Connection, "INSERT INTO cards (column_id, created_at, updated_at) VALUES (1, 1, 1);");
@@ -153,8 +138,7 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void CardTagDuplicateAssignmentIsRejected()
- {
+ public void CardTagDuplicateAssignmentIsRejected() {
using var db = new FreshDatabase();
SqliteTestHelper.Exec(db.Connection, "INSERT INTO columns (title, created_at, updated_at) VALUES ('c', 1, 1);");
SqliteTestHelper.Exec(db.Connection, "INSERT INTO cards (column_id, created_at, updated_at) VALUES (1, 1, 1);");
@@ -167,8 +151,7 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void DeletingColumnCascadesToCardsAndCardTags()
- {
+ public void DeletingColumnCascadesToCardsAndCardTags() {
using var db = new FreshDatabase();
InsertBoardRow(db.Connection);
@@ -182,8 +165,7 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void DeletingCardCascadesToCardTagsOnly()
- {
+ public void DeletingCardCascadesToCardTagsOnly() {
using var db = new FreshDatabase();
InsertBoardRow(db.Connection);
@@ -196,8 +178,7 @@ public class WorkspaceSchemaTests
}
[TestMethod]
- public void DeletingTagRemovesAssignmentsButKeepsCards()
- {
+ public void DeletingTagRemovesAssignmentsButKeepsCards() {
using var db = new FreshDatabase();
InsertBoardRow(db.Connection);
@@ -214,8 +195,7 @@ public class WorkspaceSchemaTests
/// Inserts one column, one card, one tag and their assignment, ready for a cascade test.
///
/// An open workspace-schema connection.
- private static void InsertBoardRow(SqliteConnection connection)
- {
+ private static void InsertBoardRow(SqliteConnection connection) {
SqliteTestHelper.Exec(connection, "INSERT INTO columns (title, created_at, updated_at) VALUES ('c', 1, 1);");
SqliteTestHelper.Exec(connection, "INSERT INTO cards (column_id, created_at, updated_at) VALUES (1, 1, 1);");
SqliteTestHelper.Exec(connection, "INSERT INTO tags (name, color) VALUES ('bug', '#112233');");
diff --git a/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs b/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs
index 7bade97..e24c618 100644
--- a/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs
+++ b/YKanBan.Tests/Storage/Workspace/WorkspaceSessionTests.cs
@@ -10,11 +10,9 @@ namespace YKanBan.Tests.Storage.Workspace;
/// the whole session and the repository is usable through it.
///
[TestClass]
-public class WorkspaceSessionTests
-{
+public class WorkspaceSessionTests {
[TestMethod]
- public void OpenHoldsLockUntilDisposed()
- {
+ public void OpenHoldsLockUntilDisposed() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
@@ -25,8 +23,7 @@ public class WorkspaceSessionTests
}
[TestMethod]
- public void DisposeReleasesLockForReopen()
- {
+ public void DisposeReleasesLockForReopen() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
@@ -37,8 +34,7 @@ public class WorkspaceSessionTests
}
[TestMethod]
- public void OpenUninitializedFolderThrowsNotInitialized()
- {
+ public void OpenUninitializedFolderThrowsNotInitialized() {
using var directory = new TempDirectory();
Assert.ThrowsExactly(
@@ -46,8 +42,7 @@ public class WorkspaceSessionTests
}
[TestMethod]
- public void RepositoryIsUsableThroughSession()
- {
+ public void RepositoryIsUsableThroughSession() {
using var directory = new TempDirectory();
WorkspaceInitializer.Initialize(directory.FullPath);
diff --git a/YKanBan.Tests/TestUtilities/SqliteTestHelper.cs b/YKanBan.Tests/TestUtilities/SqliteTestHelper.cs
index 72db4dc..3cef2e8 100644
--- a/YKanBan.Tests/TestUtilities/SqliteTestHelper.cs
+++ b/YKanBan.Tests/TestUtilities/SqliteTestHelper.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+
using YKanBan.Storage;
using YKanBan.Storage.Workspace;
@@ -8,8 +9,7 @@ namespace YKanBan.Tests.TestUtilities;
/// Shared helpers for the storage tests: small SQL conveniences plus opening a
/// workspace database with the real migration list.
///
-public static class SqliteTestHelper
-{
+public static class SqliteTestHelper {
///
/// Executes a non-query SQL statement and returns the affected row count.
///
@@ -17,12 +17,10 @@ public static class SqliteTestHelper
/// The statement to execute.
/// Name/value pairs bound as SQL parameters.
/// The number of affected rows.
- public static int Exec(SqliteConnection connection, string sql, params (string Name, object? Value)[] parameters)
- {
+ public static int Exec(SqliteConnection connection, string sql, params (string Name, object? Value)[] parameters) {
using var command = connection.CreateCommand();
command.CommandText = sql;
- foreach ((string name, object? value) in parameters)
- {
+ foreach ((string name, object? value) in parameters) {
command.Parameters.AddWithValue(name, value);
}
return command.ExecuteNonQuery();
@@ -34,8 +32,7 @@ public static class SqliteTestHelper
/// An open database connection.
/// The query to execute.
/// The scalar result cast to a string.
- public static string ScalarString(SqliteConnection connection, string sql)
- {
+ public static string ScalarString(SqliteConnection connection, string sql) {
using var command = connection.CreateCommand();
command.CommandText = sql;
return (string)command.ExecuteScalar()!;
@@ -47,8 +44,7 @@ public static class SqliteTestHelper
/// An open database connection.
/// The query to execute.
/// The scalar result cast to a long.
- public static long ScalarLong(SqliteConnection connection, string sql)
- {
+ public static long ScalarLong(SqliteConnection connection, string sql) {
using var command = connection.CreateCommand();
command.CommandText = sql;
return (long)command.ExecuteScalar()!;
@@ -60,8 +56,7 @@ public static class SqliteTestHelper
/// An open database connection.
/// The table name to look for.
/// when the table exists.
- public static bool TableExists(SqliteConnection connection, string tableName)
- {
+ public static bool TableExists(SqliteConnection connection, string tableName) {
using var command = connection.CreateCommand();
command.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=$name;";
command.Parameters.AddWithValue("$name", tableName);
@@ -73,15 +68,13 @@ public static class SqliteTestHelper
///
/// An open database connection.
/// The column titles in id order.
- public static List GetColumnTitles(SqliteConnection connection)
- {
+ public static List GetColumnTitles(SqliteConnection connection) {
using var command = connection.CreateCommand();
command.CommandText = "SELECT title FROM columns ORDER BY id ASC;";
using var reader = command.ExecuteReader();
var titles = new List();
- while (reader.Read())
- {
+ while (reader.Read()) {
titles.Add(reader.GetString(0));
}
return titles;
@@ -92,15 +85,13 @@ public static class SqliteTestHelper
///
/// An open database connection.
/// The column descriptions in id order.
- public static List GetColumnDescriptions(SqliteConnection connection)
- {
+ public static List GetColumnDescriptions(SqliteConnection connection) {
using var command = connection.CreateCommand();
command.CommandText = "SELECT description FROM columns ORDER BY id ASC;";
using var reader = command.ExecuteReader();
var descriptions = new List();
- while (reader.Read())
- {
+ while (reader.Read()) {
descriptions.Add(reader.GetString(0));
}
return descriptions;
@@ -111,15 +102,13 @@ public static class SqliteTestHelper
///
/// An open database connection.
/// The tags in id order.
- public static List<(string Name, string Color, string Description)> GetTags(SqliteConnection connection)
- {
+ public static List<(string Name, string Color, string Description)> GetTags(SqliteConnection connection) {
using var command = connection.CreateCommand();
command.CommandText = "SELECT name, color, description FROM tags ORDER BY id ASC;";
using var reader = command.ExecuteReader();
var tags = new List<(string, string, string)>();
- while (reader.Read())
- {
+ while (reader.Read()) {
tags.Add((reader.GetString(0), reader.GetString(1), reader.GetString(2)));
}
return tags;
@@ -130,8 +119,7 @@ public static class SqliteTestHelper
///
/// The workspace folder path.
/// An open, migrated connection.
- public static SqliteConnection OpenWorkspace(string folderPath)
- {
+ public static SqliteConnection OpenWorkspace(string folderPath) {
return SqliteDatabase.Open(WorkspacePaths.Database(folderPath), WorkspaceSchema.Migrations);
}
}
diff --git a/YKanBan.Tests/TestUtilities/TempDirectory.cs b/YKanBan.Tests/TestUtilities/TempDirectory.cs
index fe5b79c..6971a83 100644
--- a/YKanBan.Tests/TestUtilities/TempDirectory.cs
+++ b/YKanBan.Tests/TestUtilities/TempDirectory.cs
@@ -5,15 +5,13 @@ namespace YKanBan.Tests.TestUtilities;
/// disposal. The removal is retried once because SQLite WAL sidecar files can
/// linger for a moment after the connections close.
///
-public sealed class TempDirectory : IDisposable
-{
+public sealed class TempDirectory : IDisposable {
///
/// Gets the unique directory path managed by this instance.
///
public string FullPath { get; }
- public TempDirectory()
- {
+ public TempDirectory() {
FullPath = Path.Combine(Path.GetTempPath(), "ykanban-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(FullPath);
}
@@ -21,26 +19,19 @@ public sealed class TempDirectory : IDisposable
///
/// Deletes the directory tree, retrying once after a short delay.
///
- public void Dispose()
- {
- try
- {
+ public void Dispose() {
+ try {
// SAFETY: Recursive directory delete is safe: FullPath is the
// read-only, GUID-named directory this instance created under the
// OS temp folder, so it can never target user data or the temp root.
Directory.Delete(FullPath, recursive: true);
- }
- catch (IOException)
- {
+ } catch (IOException) {
Thread.Sleep(100);
- try
- {
+ try {
// SAFETY: Retry of the same recursive delete, still confined to
// this instance's own temp directory.
Directory.Delete(FullPath, recursive: true);
- }
- catch (IOException)
- {
+ } catch (IOException) {
// Leftovers in the OS temp folder are harmless; never fail a test over cleanup.
}
}
diff --git a/YKanBan/App.axaml.cs b/YKanBan/App.axaml.cs
index 1977aec..8bffc75 100644
--- a/YKanBan/App.axaml.cs
+++ b/YKanBan/App.axaml.cs
@@ -1,20 +1,22 @@
using System.Globalization;
+
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Styling;
+
using YKanBan.Services;
using YKanBan.Storage.AppData;
using YKanBan.ViewModels;
using YKanBan.Views;
+
using Resx = YKanBan.Resources;
namespace YKanBan;
-public partial class App : Application
-{
+public partial class App : Application {
///
/// Gets the process-wide composition root, created at startup.
///
@@ -26,15 +28,12 @@ public partial class App : Application
///
public static string StartupLanguage { get; private set; } = AppConfig.DefaultLanguage;
- public override void Initialize()
- {
+ public override void Initialize() {
AvaloniaXamlLoader.Load(this);
}
- public override void OnFrameworkInitializationCompleted()
- {
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
- {
+ public override void OnFrameworkInitializationCompleted() {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
DisableAvaloniaDataAnnotationValidation();
// Startup order: apply the configured culture, its RTL flow direction
@@ -46,8 +45,7 @@ public partial class App : Application
ApplyTheme(Services.Config.Theme);
var viewModel = new MainWindowViewModel(Services, desktop.Args ?? []);
- desktop.MainWindow = new MainWindow
- {
+ desktop.MainWindow = new MainWindow {
DataContext = viewModel,
// RTL detection drives the whole window flow direction; the two
@@ -67,14 +65,10 @@ public partial class App : Application
///
/// The language code from app.json.
/// The resolved culture.
- public static CultureInfo ResolveCulture(string language)
- {
- try
- {
+ public static CultureInfo ResolveCulture(string language) {
+ try {
return new CultureInfo(language);
- }
- catch (CultureNotFoundException)
- {
+ } catch (CultureNotFoundException) {
return new CultureInfo(AppConfig.DefaultLanguage);
}
}
@@ -84,28 +78,23 @@ public partial class App : Application
/// inherits the operating system preference.
///
/// The theme selection.
- public static void ApplyTheme(ThemeOption option)
- {
- if (Current is null)
- {
+ public static void ApplyTheme(ThemeOption option) {
+ if (Current is null) {
return;
}
- Current.RequestedThemeVariant = option switch
- {
+ Current.RequestedThemeVariant = option switch {
ThemeOption.Light => ThemeVariant.Light,
ThemeOption.Dark => ThemeVariant.Dark,
_ => ThemeVariant.Default,
};
}
- private static void DisableAvaloniaDataAnnotationValidation()
- {
+ private static void DisableAvaloniaDataAnnotationValidation() {
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType().ToArray();
- foreach (var plugin in dataValidationPluginsToRemove)
- {
+ foreach (var plugin in dataValidationPluginsToRemove) {
BindingPlugins.DataValidators.Remove(plugin);
}
}
diff --git a/YKanBan/Export/MarkdownExporter.cs b/YKanBan/Export/MarkdownExporter.cs
index e126545..5576cb0 100644
--- a/YKanBan/Export/MarkdownExporter.cs
+++ b/YKanBan/Export/MarkdownExporter.cs
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text;
+
using YKanBan.Models;
namespace YKanBan.Export;
@@ -11,8 +12,7 @@ namespace YKanBan.Export;
/// timestamps use the shared minute-precision pattern. The output is meant for
/// reading only — there is no re-import.
///
-public static class MarkdownExporter
-{
+public static class MarkdownExporter {
///
/// Renders the whole board to Markdown.
///
@@ -27,8 +27,7 @@ public static class MarkdownExporter
IReadOnlyList columns,
IReadOnlyList cards,
IReadOnlyList<(TagModel Tag, long UsageCount)> tags,
- DateTimeOffset exportedAt)
- {
+ DateTimeOffset exportedAt) {
CultureInfo culture = Resources.Culture ?? CultureInfo.CurrentUICulture;
var builder = new StringBuilder();
@@ -40,18 +39,15 @@ public static class MarkdownExporter
builder.AppendLine();
// Board body: every column in id order, empty ones included.
- foreach (ColumnModel column in columns)
- {
+ foreach (ColumnModel column in columns) {
IReadOnlyList cardsInColumn = cards.Where(card => card.ColumnId == column.Id).ToArray();
builder.AppendLine(string.Format(culture, Resources.Export_ColumnHeading, column.Title, cardsInColumn.Count));
- if (column.Description.Length > 0)
- {
+ if (column.Description.Length > 0) {
builder.AppendLine(SingleLine(column.Description));
}
builder.AppendLine();
- foreach (CardModel card in cardsInColumn)
- {
+ foreach (CardModel card in cardsInColumn) {
AppendCard(builder, card, culture);
}
}
@@ -67,8 +63,7 @@ public static class MarkdownExporter
/// Workspace display name.
/// Export moment.
/// The file name.
- public static string BuildFileName(string workspaceName, DateTimeOffset exportedAt)
- {
+ public static string BuildFileName(string workspaceName, DateTimeOffset exportedAt) {
CultureInfo culture = Resources.Culture ?? CultureInfo.CurrentUICulture;
return string.Format(culture, Resources.Export_FileNamePattern, workspaceName, exportedAt);
}
@@ -79,20 +74,17 @@ public static class MarkdownExporter
/// The output builder.
/// The card to render.
/// The UI culture.
- private static void AppendCard(StringBuilder builder, CardModel card, CultureInfo culture)
- {
+ private static void AppendCard(StringBuilder builder, CardModel card, CultureInfo culture) {
// Heading: "#id title"; an untitled card shows only the id.
builder.AppendLine(card.Title.Length == 0
? $"### #{card.Id}"
: $"### #{card.Id} {SingleLine(card.Title)}");
- if (card.Content.Length > 0)
- {
+ if (card.Content.Length > 0) {
builder.AppendLine(card.Content);
}
- if (card.Tags.Count > 0)
- {
+ if (card.Tags.Count > 0) {
string names = string.Join(" ", card.Tags.Select(tag => $"`{tag.Name}`"));
builder.AppendLine(string.Format(culture, Resources.Export_TagsLine, names));
}
@@ -112,13 +104,11 @@ public static class MarkdownExporter
private static void AppendTagTable(
StringBuilder builder,
IReadOnlyList<(TagModel Tag, long UsageCount)> tags,
- CultureInfo culture)
- {
+ CultureInfo culture) {
builder.AppendLine(Resources.Export_TagsSummaryHeading);
builder.AppendLine(Resources.Export_TagsTableHeader);
builder.AppendLine("|---|---|---|---|");
- foreach ((TagModel tag, long usage) in tags)
- {
+ foreach ((TagModel tag, long usage) in tags) {
// '|' and line breaks would break the table row; neutralize them.
builder.AppendLine($"| {Cell(tag.Name)} | {tag.Color} | {Cell(tag.Description)} | {usage} |");
}
diff --git a/YKanBan/Launching/LaunchArguments.cs b/YKanBan/Launching/LaunchArguments.cs
index e96d3e6..488d96c 100644
--- a/YKanBan/Launching/LaunchArguments.cs
+++ b/YKanBan/Launching/LaunchArguments.cs
@@ -8,8 +8,7 @@ namespace YKanBan.Launching;
/// more than one argument, or a blank argument is rejected as an argument
/// error. The path itself is not checked for existence here.
///
-public static class LaunchArguments
-{
+public static class LaunchArguments {
///
/// Tries to parse command-line arguments following the
/// ykanban <path> contract.
@@ -17,10 +16,8 @@ public static class LaunchArguments
/// The raw arguments, excluding the executable name.
/// The parsed workspace path on success, otherwise .
/// when exactly one non-blank path was supplied.
- public static bool TryParse(IReadOnlyList arguments, [NotNullWhen(true)] out string? folderPath)
- {
- if (arguments.Count == 1 && !string.IsNullOrWhiteSpace(arguments[0]))
- {
+ public static bool TryParse(IReadOnlyList arguments, [NotNullWhen(true)] out string? folderPath) {
+ if (arguments.Count == 1 && !string.IsNullOrWhiteSpace(arguments[0])) {
folderPath = arguments[0];
return true;
}
diff --git a/YKanBan/Models/RgbColor.cs b/YKanBan/Models/RgbColor.cs
index b070fc3..e3ec758 100644
--- a/YKanBan/Models/RgbColor.cs
+++ b/YKanBan/Models/RgbColor.cs
@@ -10,8 +10,7 @@ namespace YKanBan.Models;
/// always produces the canonical lowercase form (for example "#ff0a00"). It is
/// not specific to tags and may be reused anywhere a color value is needed.
///
-public readonly record struct RgbColor(byte R, byte G, byte B)
-{
+public readonly record struct RgbColor(byte R, byte G, byte B) {
///
/// Parses a "#RRGGBB" text with case-insensitive hex digits.
///
@@ -29,17 +28,14 @@ public readonly record struct RgbColor(byte R, byte G, byte B)
/// The text to parse.
/// The parsed color on success, otherwise the default value.
/// when the text is exactly #RRGGBB.
- public static bool TryParse(string? text, out RgbColor color)
- {
+ public static bool TryParse(string? text, out RgbColor color) {
color = default;
- if (text is not { Length: 7 } || text[0] != '#')
- {
+ if (text is not { Length: 7 } || text[0] != '#') {
return false;
}
// Two hex digits per component: R, G, B.
- if (TryHexPair(text, 1, out byte r) && TryHexPair(text, 3, out byte g) && TryHexPair(text, 5, out byte b))
- {
+ if (TryHexPair(text, 1, out byte r) && TryHexPair(text, 3, out byte g) && TryHexPair(text, 5, out byte b)) {
color = new RgbColor(r, g, b);
return true;
}
@@ -59,13 +55,11 @@ public readonly record struct RgbColor(byte R, byte G, byte B)
/// Index of the first of the two digits.
/// The parsed byte value.
/// when both characters are hex digits.
- private static bool TryHexPair(string text, int start, out byte value)
- {
+ private static bool TryHexPair(string text, int start, out byte value) {
value = 0;
int high = HexDigit(text[start]);
int low = HexDigit(text[start + 1]);
- if (high < 0 || low < 0)
- {
+ if (high < 0 || low < 0) {
return false;
}
value = (byte)((high << 4) | low);
@@ -77,8 +71,7 @@ public readonly record struct RgbColor(byte R, byte G, byte B)
///
/// The character to convert.
/// The digit value, or -1 for any other character.
- private static int HexDigit(char character) => character switch
- {
+ private static int HexDigit(char character) => character switch {
>= '0' and <= '9' => character - '0',
>= 'a' and <= 'f' => character - 'a' + 10,
>= 'A' and <= 'F' => character - 'A' + 10,
diff --git a/YKanBan/Program.cs b/YKanBan/Program.cs
index 8a1170b..e606f36 100644
--- a/YKanBan/Program.cs
+++ b/YKanBan/Program.cs
@@ -2,14 +2,12 @@
namespace YKanBan;
-sealed class Program
-{
+sealed class Program {
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
- public static void Main(string[] args)
- {
+ public static void Main(string[] args) {
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// The lifetime has ended: persist the configuration once, then release services.
diff --git a/YKanBan/Search/SearchLexer.cs b/YKanBan/Search/SearchLexer.cs
index 0da560b..a80d576 100644
--- a/YKanBan/Search/SearchLexer.cs
+++ b/YKanBan/Search/SearchLexer.cs
@@ -5,8 +5,7 @@ namespace YKanBan.Search;
///
/// Lexical tokens of the search syntax.
///
-public abstract record SearchToken
-{
+public abstract record SearchToken {
///
/// A bare word; qualifier detection happens later from the text.
///
@@ -39,14 +38,17 @@ public abstract record SearchToken
/// unterminated phrase is rejected. Any input not covered by the token rules
/// is rejected rather than silently ignored.
///
-public static class SearchLexer
-{
- /// A valid phrase escape sequence: \" or \\.
+public static class SearchLexer {
+ ///
+ /// A valid phrase escape sequence: \" or \\.
+ ///
private static readonly Parser PhraseEscape =
Parser.Try(Parser.String("\\\"").Select(_ => '"'))
.Or(Parser.String("\\\\").Select(_ => '\\'));
- /// One phrase character: an escape or any character except a quote or backslash.
+ ///
+ /// One phrase character: an escape or any character except a quote or backslash.
+ ///
private static readonly Parser PhraseCharacter =
PhraseEscape.Or(Parser.Token(character => character != '"' && character != '\\'));
@@ -90,14 +92,10 @@ public static class SearchLexer
/// The raw search text.
/// The token stream.
/// The text contains an invalid escape or an unterminated phrase.
- public static IReadOnlyList Tokenize(string input)
- {
- try
- {
+ public static IReadOnlyList Tokenize(string input) {
+ try {
return Lexer.ParseOrThrow(input);
- }
- catch (ParseException exception)
- {
+ } catch (ParseException exception) {
throw new SearchSyntaxException(exception.Message, exception);
}
}
diff --git a/YKanBan/Search/SearchNodes.cs b/YKanBan/Search/SearchNodes.cs
index 5609615..eb56fbf 100644
--- a/YKanBan/Search/SearchNodes.cs
+++ b/YKanBan/Search/SearchNodes.cs
@@ -10,8 +10,7 @@ namespace YKanBan.Search;
/// Root of the search query AST. Search keywords are fixed English grammar
/// and are never localized.
///
-public abstract record SearchNode
-{
+public abstract record SearchNode {
///
/// Disjunction: matches when any term matches (OR).
///
@@ -59,11 +58,14 @@ public abstract record SearchNode
///
/// The card fields a atom can scope to.
///
-public enum SearchField
-{
- /// The card title.
+public enum SearchField {
+ ///
+ /// The card title.
+ ///
Title,
- /// The card content body.
+ ///
+ /// The card content body.
+ ///
Content,
}
diff --git a/YKanBan/Search/SearchQueryParser.cs b/YKanBan/Search/SearchQueryParser.cs
index 47b8054..d9efbc7 100644
--- a/YKanBan/Search/SearchQueryParser.cs
+++ b/YKanBan/Search/SearchQueryParser.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+
using Pidgin;
namespace YKanBan.Search;
@@ -14,9 +15,10 @@ namespace YKanBan.Search;
/// input returns null and means "show everything". Search keywords and
/// qualifier keys are fixed English grammar and are never localized.
///
-public static class SearchQueryParser
-{
- /// The recognized qualifier keys; add new entries here to extend the grammar.
+public static class SearchQueryParser {
+ ///
+ /// The recognized qualifier keys; add new entries here to extend the grammar.
+ ///
private static readonly (string Key, QualifierKind Kind)[] QualifierTable =
[
("tag", QualifierKind.Tag),
@@ -40,12 +42,16 @@ public static class SearchQueryParser
private static readonly Parser CloseParen =
Parser.Token(token => token is SearchToken.CloseParen);
- /// The text of a phrase token.
+ ///
+ /// The text of a phrase token.
+ ///
private static readonly Parser PhraseText =
Parser.Token(token => token is SearchToken.Phrase)
.Select(token => ((SearchToken.Phrase)token).Text);
- /// The text of a word token, operator keywords excluded (they never start an operand).
+ ///
+ /// The text of a word token, operator keywords excluded (they never start an operand).
+ ///
private static readonly Parser NonKeywordWord =
Parser.Token(token => token is SearchToken.Word word && word.Text != "AND" && word.Text != "OR")
.Select(token => ((SearchToken.Word)token).Text);
@@ -90,7 +96,9 @@ public static class SearchQueryParser
private static readonly Parser Query =
OrExpr.Before(Parser.End);
- /// Defers the OrExpr field read to parse time (recursion cycle breaker).
+ ///
+ /// Defers the OrExpr field read to parse time (recursion cycle breaker).
+ ///
/// The OR-expression parser.
private static Parser OrExprDeferred() => OrExpr;
@@ -102,20 +110,15 @@ public static class SearchQueryParser
/// The raw search text.
/// The parsed query, or for blank input (meaning: show everything).
/// The text violates the grammar.
- public static SearchNode? Parse(string text)
- {
- if (string.IsNullOrWhiteSpace(text))
- {
+ public static SearchNode? Parse(string text) {
+ if (string.IsNullOrWhiteSpace(text)) {
return null;
}
- try
- {
+ try {
List tokens = SearchLexer.Tokenize(text).ToList();
return Query.ParseOrThrow(tokens);
- }
- catch (ParseException exception)
- {
+ } catch (ParseException exception) {
throw new SearchSyntaxException(exception.Message, exception);
}
}
@@ -128,21 +131,18 @@ public static class SearchQueryParser
///
/// The word token text.
/// The parser producing the operand node.
- private static Parser QualifierFrom(string word)
- {
+ private static Parser QualifierFrom(string word) {
int colonIndex = word.IndexOf(':');
// Only a leading run of letters/digits/underscores before ':' can be a qualifier key.
- if (colonIndex <= 0 || !IsQualifierKeyCandidate(word.AsSpan(0, colonIndex)))
- {
+ if (colonIndex <= 0 || !IsQualifierKeyCandidate(word.AsSpan(0, colonIndex))) {
return Parser.Return(new SearchNode.FreeText(word));
}
string key = word[..colonIndex];
string value = word[(colonIndex + 1)..];
- if (!TryLookupQualifier(key, out QualifierKind kind))
- {
+ if (!TryLookupQualifier(key, out QualifierKind kind)) {
return Parser.Fail($"Unknown qualifier key '{key}'.");
}
@@ -169,8 +169,7 @@ public static class SearchQueryParser
/// The qualifier kind.
/// The qualifier value.
/// The atom node, or for an invalid id value.
- private static SearchNode? AtomFrom(QualifierKind kind, string value) => kind switch
- {
+ private static SearchNode? AtomFrom(QualifierKind kind, string value) => kind switch {
QualifierKind.Tag => new SearchNode.TagName(value),
QualifierKind.Title => new SearchNode.FieldText(SearchField.Title, value),
QualifierKind.Content => new SearchNode.FieldText(SearchField.Content, value),
@@ -186,12 +185,9 @@ public static class SearchQueryParser
/// The key text before the colon.
/// The matching qualifier kind.
/// when the key is known.
- private static bool TryLookupQualifier(string key, out QualifierKind kind)
- {
- foreach ((string candidate, QualifierKind candidateKind) in QualifierTable)
- {
- if (string.Equals(candidate, key, StringComparison.Ordinal))
- {
+ private static bool TryLookupQualifier(string key, out QualifierKind kind) {
+ foreach ((string candidate, QualifierKind candidateKind) in QualifierTable) {
+ if (string.Equals(candidate, key, StringComparison.Ordinal)) {
kind = candidateKind;
return true;
}
@@ -207,12 +203,9 @@ public static class SearchQueryParser
///
/// The candidate key text.
/// when every character is a word character.
- private static bool IsQualifierKeyCandidate(ReadOnlySpan text)
- {
- foreach (char character in text)
- {
- if (!(char.IsLetterOrDigit(character) || character == '_'))
- {
+ private static bool IsQualifierKeyCandidate(ReadOnlySpan text) {
+ foreach (char character in text) {
+ if (!(char.IsLetterOrDigit(character) || character == '_')) {
return false;
}
}
@@ -226,8 +219,7 @@ public static class SearchQueryParser
/// The first operand.
/// The remaining operands.
/// The single operand, or an node.
- private static SearchNode FoldAnd(SearchNode first, IEnumerable rest)
- {
+ private static SearchNode FoldAnd(SearchNode first, IEnumerable rest) {
List terms = [first, .. rest];
return terms.Count == 1 ? terms[0] : new SearchNode.And(terms);
}
@@ -237,17 +229,17 @@ public static class SearchQueryParser
///
/// The operands.
/// The single operand, or a node.
- private static SearchNode FoldOr(IEnumerable terms)
- {
+ private static SearchNode FoldOr(IEnumerable terms) {
List list = terms.ToList();
return list.Count == 1 ? list[0] : new SearchNode.Or(list);
}
#endregion
- /// The recognized qualifier kinds.
- private enum QualifierKind
- {
+ ///
+ /// The recognized qualifier kinds.
+ ///
+ private enum QualifierKind {
Tag,
Title,
Content,
diff --git a/YKanBan/Search/SearchSqlCompiler.cs b/YKanBan/Search/SearchSqlCompiler.cs
index 95530fe..a9692cb 100644
--- a/YKanBan/Search/SearchSqlCompiler.cs
+++ b/YKanBan/Search/SearchSqlCompiler.cs
@@ -15,15 +15,13 @@ public sealed record CompiledSearch(string Predicate, IReadOnlyList<(string Name
/// wildcards and the escape character in user text are escaped so they match
/// literally.
///
-public static class SearchSqlCompiler
-{
+public static class SearchSqlCompiler {
///
/// Compiles the query into a SQL predicate.
///
/// The parsed search AST.
/// The compiled predicate and its parameters.
- public static CompiledSearch Compile(SearchNode query)
- {
+ public static CompiledSearch Compile(SearchNode query) {
var parameters = new List<(string Name, object Value)>();
string predicate = Build(query, parameters);
return new CompiledSearch(predicate, parameters);
@@ -35,49 +33,42 @@ public static class SearchSqlCompiler
/// The AST node to compile.
/// The parameter accumulator.
/// The SQL boolean expression for the node.
- private static string Build(SearchNode node, List<(string Name, object Value)> parameters)
- {
- switch (node)
- {
+ private static string Build(SearchNode node, List<(string Name, object Value)> parameters) {
+ switch (node) {
case SearchNode.And and:
return "(" + string.Join(" AND ", and.Terms.Select(term => Build(term, parameters))) + ")";
case SearchNode.Or or:
return "(" + string.Join(" OR ", or.Terms.Select(term => Build(term, parameters))) + ")";
- case SearchNode.FreeText freeText:
- {
- // A bare word matches a substring of the title OR the content.
- string titleParam = AddParameter(parameters, "%" + EscapeLike(freeText.Text) + "%");
- string contentParam = AddParameter(parameters, "%" + EscapeLike(freeText.Text) + "%");
- return $"(c.title LIKE {titleParam} ESCAPE '\\' OR c.content LIKE {contentParam} ESCAPE '\\')";
- }
+ case SearchNode.FreeText freeText: {
+ // A bare word matches a substring of the title OR the content.
+ string titleParam = AddParameter(parameters, "%" + EscapeLike(freeText.Text) + "%");
+ string contentParam = AddParameter(parameters, "%" + EscapeLike(freeText.Text) + "%");
+ return $"(c.title LIKE {titleParam} ESCAPE '\\' OR c.content LIKE {contentParam} ESCAPE '\\')";
+ }
- case SearchNode.FieldText field:
- {
- string column = field.Field == SearchField.Title ? "c.title" : "c.content";
- string param = AddParameter(parameters, "%" + EscapeLike(field.Text) + "%");
- return $"{column} LIKE {param} ESCAPE '\\'";
- }
+ case SearchNode.FieldText field: {
+ string column = field.Field == SearchField.Title ? "c.title" : "c.content";
+ string param = AddParameter(parameters, "%" + EscapeLike(field.Text) + "%");
+ return $"{column} LIKE {param} ESCAPE '\\'";
+ }
- case SearchNode.TagName tag:
- {
- string param = AddParameter(parameters, tag.Name);
- return "EXISTS (SELECT 1 FROM card_tags ct JOIN tags t ON t.id = ct.tag_id " +
- $"WHERE ct.card_id = c.id AND t.name COLLATE NOCASE = {param})";
- }
+ case SearchNode.TagName tag: {
+ string param = AddParameter(parameters, tag.Name);
+ return "EXISTS (SELECT 1 FROM card_tags ct JOIN tags t ON t.id = ct.tag_id " +
+ $"WHERE ct.card_id = c.id AND t.name COLLATE NOCASE = {param})";
+ }
- case SearchNode.ColumnTitle column:
- {
- string param = AddParameter(parameters, column.Title);
- return $"c.column_id IN (SELECT id FROM columns WHERE title COLLATE NOCASE = {param})";
- }
+ case SearchNode.ColumnTitle column: {
+ string param = AddParameter(parameters, column.Title);
+ return $"c.column_id IN (SELECT id FROM columns WHERE title COLLATE NOCASE = {param})";
+ }
- case SearchNode.CardId id:
- {
- string param = AddParameter(parameters, id.Id);
- return $"c.id = {param}";
- }
+ case SearchNode.CardId id: {
+ string param = AddParameter(parameters, id.Id);
+ return $"c.id = {param}";
+ }
default:
throw new InvalidOperationException($"Unsupported search node: {node.GetType().Name}");
@@ -90,8 +81,7 @@ public static class SearchSqlCompiler
/// The parameter accumulator.
/// The value to bind.
/// The placeholder text.
- private static string AddParameter(List<(string Name, object Value)> parameters, object value)
- {
+ private static string AddParameter(List<(string Name, object Value)> parameters, object value) {
string name = "$p" + parameters.Count.ToString(System.Globalization.CultureInfo.InvariantCulture);
parameters.Add((name, value));
return name;
diff --git a/YKanBan/Search/SearchSyntaxException.cs b/YKanBan/Search/SearchSyntaxException.cs
index 483d89a..e27591c 100644
--- a/YKanBan/Search/SearchSyntaxException.cs
+++ b/YKanBan/Search/SearchSyntaxException.cs
@@ -6,15 +6,13 @@ namespace YKanBan.Search;
/// search error instead of coupling to Pidgin types. The message is never
/// localized.
///
-public sealed class SearchSyntaxException : Exception
-{
+public sealed class SearchSyntaxException : Exception {
///
/// Initializes the exception with a descriptive English message.
///
/// The English diagnostic message.
/// The underlying parse failure.
public SearchSyntaxException(string message, Exception innerException)
- : base(message, innerException)
- {
+ : base(message, innerException) {
}
}
diff --git a/YKanBan/Services/AppServices.cs b/YKanBan/Services/AppServices.cs
index ac6b593..e9c1bb2 100644
--- a/YKanBan/Services/AppServices.cs
+++ b/YKanBan/Services/AppServices.cs
@@ -8,13 +8,11 @@ namespace YKanBan.Services;
/// the single open workspace session. The configuration is persisted once at
/// shutdown; the session (lock plus connection) lives until disposal.
///
-public sealed class AppServices : IDisposable
-{
+public sealed class AppServices : IDisposable {
///
/// Loads the configuration and prepares the service graph.
///
- public AppServices()
- {
+ public AppServices() {
ConfigStore = new AppConfigStore(AppDataPaths.AppConfigJsonPath);
Config = ConfigStore.Load();
}
@@ -42,8 +40,7 @@ public sealed class AppServices : IDisposable
/// The open session.
/// The folder has no .ykanban structure.
/// Another instance already holds the lock.
- public WorkspaceSession OpenWorkspace(string folderPath)
- {
+ public WorkspaceSession OpenWorkspace(string folderPath) {
Session = WorkspaceSession.Open(folderPath);
return Session;
}
diff --git a/YKanBan/Storage/AppData/AppConfig.cs b/YKanBan/Storage/AppData/AppConfig.cs
index 347360f..e2226ca 100644
--- a/YKanBan/Storage/AppData/AppConfig.cs
+++ b/YKanBan/Storage/AppData/AppConfig.cs
@@ -1,4 +1,5 @@
using System.Runtime.Serialization;
+
using Newtonsoft.Json;
namespace YKanBan.Storage.AppData;
@@ -7,37 +8,52 @@ namespace YKanBan.Storage.AppData;
/// Display ordering options, used for both cards and columns. Except for the id
/// order, every option falls back to id ascending as the final tie-breaker.
///
-public enum SortOption
-{
- /// Primary key ascending.
+public enum SortOption {
+ ///
+ /// Primary key ascending.
+ ///
[EnumMember(Value = "id-asc")]
IdAscending,
- /// Primary key descending.
+ ///
+ /// Primary key descending.
+ ///
[EnumMember(Value = "id-desc")]
IdDescending,
- /// Creation time ascending.
+ ///
+ /// Creation time ascending.
+ ///
[EnumMember(Value = "created-asc")]
CreatedAscending,
- /// Creation time descending.
+ ///
+ /// Creation time descending.
+ ///
[EnumMember(Value = "created-desc")]
CreatedDescending,
- /// Last modification time ascending.
+ ///
+ /// Last modification time ascending.
+ ///
[EnumMember(Value = "updated-asc")]
UpdatedAscending,
- /// Last modification time descending.
+ ///
+ /// Last modification time descending.
+ ///
[EnumMember(Value = "updated-desc")]
UpdatedDescending,
- /// Title by code points (BINARY collation) ascending.
+ ///
+ /// Title by code points (BINARY collation) ascending.
+ ///
[EnumMember(Value = "title-asc")]
TitleAscending,
- /// Title by code points (BINARY collation) descending.
+ ///
+ /// Title by code points (BINARY collation) descending.
+ ///
[EnumMember(Value = "title-desc")]
TitleDescending,
}
@@ -45,21 +61,28 @@ public enum SortOption
///
/// Column width presets. uses a user-defined pixel width.
///
-public enum ColumnWidthPreset
-{
- /// Narrow preset.
+public enum ColumnWidthPreset {
+ ///
+ /// Narrow preset.
+ ///
[EnumMember(Value = "narrow")]
Narrow,
- /// Standard preset (320 px, the default).
+ ///
+ /// Standard preset (320 px, the default).
+ ///
[EnumMember(Value = "standard")]
Standard,
- /// Wide preset.
+ ///
+ /// Wide preset.
+ ///
[EnumMember(Value = "wide")]
Wide,
- /// User-defined pixel width (160–720).
+ ///
+ /// User-defined pixel width (160–720).
+ ///
[EnumMember(Value = "custom")]
Custom,
}
@@ -67,17 +90,22 @@ public enum ColumnWidthPreset
///
/// UI theme selection.
///
-public enum ThemeOption
-{
- /// Always light.
+public enum ThemeOption {
+ ///
+ /// Always light.
+ ///
[EnumMember(Value = "light")]
Light,
- /// Always dark.
+ ///
+ /// Always dark.
+ ///
[EnumMember(Value = "dark")]
Dark,
- /// Follow the operating system setting (default).
+ ///
+ /// Follow the operating system setting (default).
+ ///
[EnumMember(Value = "follow-system")]
FollowSystem,
}
@@ -85,8 +113,7 @@ public enum ThemeOption
///
/// Board column width settings, applied globally.
///
-public sealed class ColumnWidthSettings
-{
+public sealed class ColumnWidthSettings {
///
/// Gets or sets the active width preset.
///
@@ -105,8 +132,7 @@ public sealed class ColumnWidthSettings
/// Everything that controls how the board is displayed: the column width and
/// the card and column ordering.
///
-public sealed class DisplaySettings
-{
+public sealed class DisplaySettings {
///
/// Gets or sets the board column width settings.
///
@@ -129,8 +155,7 @@ public sealed class DisplaySettings
///
/// Confirmation-dialog toggles for the destructive actions.
///
-public sealed class ConfirmationSettings
-{
+public sealed class ConfirmationSettings {
///
/// Gets or sets whether deleting a card asks for confirmation.
///
@@ -162,8 +187,7 @@ public sealed class ConfirmationSettings
/// and ).
/// The version field is the migration hook for future format changes.
///
-public sealed class AppConfig
-{
+public sealed class AppConfig {
///
/// Current configuration format version.
///
diff --git a/YKanBan/Storage/AppData/AppConfigStore.cs b/YKanBan/Storage/AppData/AppConfigStore.cs
index 0ae201c..bc7e521 100644
--- a/YKanBan/Storage/AppData/AppConfigStore.cs
+++ b/YKanBan/Storage/AppData/AppConfigStore.cs
@@ -9,10 +9,8 @@ namespace YKanBan.Storage.AppData;
/// used. Saving writes the file directly (no atomic replace) — losing the last
/// session's settings to a crash is an accepted trade-off.
///
-public sealed class AppConfigStore
-{
- private static readonly JsonSerializerSettings SerializerSettings = new()
- {
+public sealed class AppConfigStore {
+ private static readonly JsonSerializerSettings SerializerSettings = new() {
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
@@ -37,21 +35,17 @@ public sealed class AppConfigStore
///
/// The loaded configuration, or defaults when none could be read.
/// The file exists but cannot be read.
- public AppConfig Load()
- {
+ public AppConfig Load() {
// Missing file is a normal first-run state: run with defaults.
- if (!File.Exists(FilePath))
- {
+ if (!File.Exists(FilePath)) {
return new AppConfig();
}
string json = File.ReadAllText(FilePath);
- try
- {
+ try {
// A JSON null literal deserializes to null; treat it like defaults.
AppConfig? config = JsonConvert.DeserializeObject(json, SerializerSettings);
- if (config is null)
- {
+ if (config is null) {
return new AppConfig();
}
@@ -59,9 +53,7 @@ public sealed class AppConfigStore
config.Display ??= new DisplaySettings();
config.Confirmations ??= new ConfirmationSettings();
return config;
- }
- catch (JsonException)
- {
+ } catch (JsonException) {
// Corrupt: keep a copy for inspection, then continue with defaults.
File.Copy(FilePath, FilePath + ".bak", overwrite: true);
return new AppConfig();
@@ -72,12 +64,10 @@ public sealed class AppConfigStore
/// Writes the configuration directly to disk (no atomic replace).
///
/// The configuration to serialize.
- public void Save(AppConfig config)
- {
+ public void Save(AppConfig config) {
// Create the parent folder on demand; normally it already exists.
string? directory = Path.GetDirectoryName(FilePath);
- if (!string.IsNullOrEmpty(directory))
- {
+ if (!string.IsNullOrEmpty(directory)) {
Directory.CreateDirectory(directory);
}
diff --git a/YKanBan/Storage/AppData/AppDataPaths.cs b/YKanBan/Storage/AppData/AppDataPaths.cs
index 40cdd1e..10fb4f6 100644
--- a/YKanBan/Storage/AppData/AppDataPaths.cs
+++ b/YKanBan/Storage/AppData/AppDataPaths.cs
@@ -5,8 +5,7 @@ namespace YKanBan.Storage.AppData;
/// (%APPDATA%/YKanBan on Windows and the equivalent directory on other
/// platforms).
///
-public static class AppDataPaths
-{
+public static class AppDataPaths {
///
/// Gets the per-user YKanBan application data folder.
///
diff --git a/YKanBan/Storage/SqliteDatabase.cs b/YKanBan/Storage/SqliteDatabase.cs
index 0b5d973..7b2b9a9 100644
--- a/YKanBan/Storage/SqliteDatabase.cs
+++ b/YKanBan/Storage/SqliteDatabase.cs
@@ -7,8 +7,7 @@ namespace YKanBan.Storage;
/// the mandated PRAGMA configuration (WAL journal, NORMAL synchronous mode,
/// foreign keys on) and applies incremental user_version migrations.
///
-public static class SqliteDatabase
-{
+public static class SqliteDatabase {
///
/// Opens (creating if needed) a database file, applies the mandatory
/// PRAGMAs and runs any pending migrations.
@@ -16,10 +15,8 @@ public static class SqliteDatabase
/// Path of the SQLite database file.
/// Contiguous migration list ordered by version, starting at 1.
/// An open connection with all PRAGMAs applied and the schema migrated.
- public static SqliteConnection Open(string databasePath, IReadOnlyList migrations)
- {
- var connection = new SqliteConnection(new SqliteConnectionStringBuilder
- {
+ public static SqliteConnection Open(string databasePath, IReadOnlyList migrations) {
+ var connection = new SqliteConnection(new SqliteConnectionStringBuilder {
DataSource = databasePath,
Mode = SqliteOpenMode.ReadWriteCreate,
@@ -28,14 +25,11 @@ public static class SqliteDatabase
}.ToString());
connection.Open();
- try
- {
+ try {
ApplyPragmas(connection);
ApplyMigrations(connection, migrations);
return connection;
- }
- catch
- {
+ } catch {
connection.Dispose();
throw;
}
@@ -46,8 +40,7 @@ public static class SqliteDatabase
///
/// An open database connection.
/// The stored schema version.
- public static long ReadUserVersion(SqliteConnection connection)
- {
+ public static long ReadUserVersion(SqliteConnection connection) {
using var command = connection.CreateCommand();
command.CommandText = "PRAGMA user_version;";
return (long)command.ExecuteScalar()!;
@@ -59,8 +52,7 @@ public static class SqliteDatabase
/// enabled on every connection or the DDL cascades never fire.
///
/// An open database connection.
- internal static void ApplyPragmas(SqliteConnection connection)
- {
+ internal static void ApplyPragmas(SqliteConnection connection) {
// WAL plus NORMAL synchronous is the chosen durability/performance trade-off.
Execute(connection, "PRAGMA journal_mode=WAL;");
Execute(connection, "PRAGMA synchronous=NORMAL;");
@@ -75,13 +67,10 @@ public static class SqliteDatabase
/// Contiguous migration list ordered by version, starting at 1.
/// The migration list is not contiguous from version 1.
/// The database was written by a newer build.
- internal static void ApplyMigrations(SqliteConnection connection, IReadOnlyList migrations)
- {
+ internal static void ApplyMigrations(SqliteConnection connection, IReadOnlyList migrations) {
// Contract check: the list must be ordered, contiguous and start at version 1.
- for (int index = 0; index < migrations.Count; index++)
- {
- if (migrations[index].Version != index + 1)
- {
+ for (int index = 0; index < migrations.Count; index++) {
+ if (migrations[index].Version != index + 1) {
throw new ArgumentException("Migrations must be contiguous and start at version 1.", nameof(migrations));
}
}
@@ -90,16 +79,13 @@ public static class SqliteDatabase
int latest = migrations.Count;
// A database written by a newer build cannot be managed by this one; refuse loudly.
- if (current > latest)
- {
+ if (current > latest) {
throw new SchemaVersionException(current, latest);
}
// Apply each pending migration atomically so a crash never leaves a half-migrated database.
- foreach (SchemaMigration migration in migrations)
- {
- if (migration.Version <= current)
- {
+ foreach (SchemaMigration migration in migrations) {
+ if (migration.Version <= current) {
continue;
}
@@ -116,8 +102,7 @@ public static class SqliteDatabase
/// An open database connection.
/// The SQL script to execute.
/// The transaction to enlist in, when any.
- private static void Execute(SqliteConnection connection, string commandText, SqliteTransaction? transaction = null)
- {
+ private static void Execute(SqliteConnection connection, string commandText, SqliteTransaction? transaction = null) {
using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = commandText;
diff --git a/YKanBan/Storage/StorageExceptions.cs b/YKanBan/Storage/StorageExceptions.cs
index ce7bfb0..bd53e05 100644
--- a/YKanBan/Storage/StorageExceptions.cs
+++ b/YKanBan/Storage/StorageExceptions.cs
@@ -5,24 +5,21 @@ namespace YKanBan.Storage;
/// Exception messages are always English and never localized; the UI maps the
/// exception type to a localized resource key instead.
///
-public abstract class YKanBanException : Exception
-{
+public abstract class YKanBanException : Exception {
///
/// Initializes the exception with an English diagnostic message.
///
/// The English diagnostic message.
/// The underlying cause, when any.
protected YKanBanException(string message, Exception? innerException = null)
- : base(message, innerException)
- {
+ : base(message, innerException) {
}
}
///
/// Signals that the folder passed on the command line does not exist on disk.
///
-public sealed class WorkspaceDirectoryMissingException : YKanBanException
-{
+public sealed class WorkspaceDirectoryMissingException : YKanBanException {
///
/// Gets the folder path that was expected to exist.
///
@@ -33,8 +30,7 @@ public sealed class WorkspaceDirectoryMissingException : YKanBanException
///
/// The missing folder path.
public WorkspaceDirectoryMissingException(string folderPath)
- : base($"The folder does not exist: {folderPath}")
- {
+ : base($"The folder does not exist: {folderPath}") {
FolderPath = folderPath;
}
}
@@ -43,8 +39,7 @@ public sealed class WorkspaceDirectoryMissingException : YKanBanException
/// Signals that a folder exists but has no .ykanban structure, so it is not
/// (yet) a workspace.
///
-public sealed class WorkspaceNotInitializedException : YKanBanException
-{
+public sealed class WorkspaceNotInitializedException : YKanBanException {
///
/// Gets the folder path that lacks the .ykanban structure.
///
@@ -55,8 +50,7 @@ public sealed class WorkspaceNotInitializedException : YKanBanException
///
/// The uninitialized folder path.
public WorkspaceNotInitializedException(string folderPath)
- : base($"The folder is not a YKanBan workspace (missing .ykanban): {folderPath}")
- {
+ : base($"The folder is not a YKanBan workspace (missing .ykanban): {folderPath}") {
FolderPath = folderPath;
}
}
@@ -65,8 +59,7 @@ public sealed class WorkspaceNotInitializedException : YKanBanException
/// Signals that another instance already holds the workspace lock, so this
/// process must not touch the workspace.
///
-public sealed class WorkspaceLockException : YKanBanException
-{
+public sealed class WorkspaceLockException : YKanBanException {
///
/// Gets the lock file that could not be acquired.
///
@@ -78,8 +71,7 @@ public sealed class WorkspaceLockException : YKanBanException
/// The lock file that is already held.
/// The underlying I/O error.
public WorkspaceLockException(string lockFilePath, Exception innerException)
- : base($"The workspace is locked by another instance: {lockFilePath}", innerException)
- {
+ : base($"The workspace is locked by another instance: {lockFilePath}", innerException) {
LockFilePath = lockFilePath;
}
}
@@ -88,8 +80,7 @@ public sealed class WorkspaceLockException : YKanBanException
/// Signals that the database was written by a newer build than the one
/// currently running, so its schema cannot be safely managed.
///
-public sealed class SchemaVersionException : YKanBanException
-{
+public sealed class SchemaVersionException : YKanBanException {
///
/// Gets the user_version value found in the database.
///
@@ -106,8 +97,7 @@ public sealed class SchemaVersionException : YKanBanException
/// The user_version read from the database.
/// The newest schema version this build supports.
public SchemaVersionException(long storedVersion, int supportedVersion)
- : base($"Database schema version {storedVersion} is newer than the latest supported version {supportedVersion}.")
- {
+ : base($"Database schema version {storedVersion} is newer than the latest supported version {supportedVersion}.") {
StoredVersion = storedVersion;
SupportedVersion = supportedVersion;
}
diff --git a/YKanBan/Storage/UnixTime.cs b/YKanBan/Storage/UnixTime.cs
index da0e3f6..6becb12 100644
--- a/YKanBan/Storage/UnixTime.cs
+++ b/YKanBan/Storage/UnixTime.cs
@@ -4,8 +4,7 @@ namespace YKanBan.Storage;
/// Supplies the current Unix timestamp. Every timestamp persisted by YKanBan
/// is an INTEGER count of Unix seconds.
///
-public static class UnixTime
-{
+public static class UnixTime {
///
/// Gets the current UTC time expressed as Unix seconds.
///
diff --git a/YKanBan/Storage/Workspace/WorkspaceInitializer.cs b/YKanBan/Storage/Workspace/WorkspaceInitializer.cs
index 25a5307..80534c4 100644
--- a/YKanBan/Storage/Workspace/WorkspaceInitializer.cs
+++ b/YKanBan/Storage/Workspace/WorkspaceInitializer.cs
@@ -8,8 +8,7 @@ namespace YKanBan.Storage.Workspace;
/// separate concern handled by , so the
/// database can be created without any rows.
///
-public static class WorkspaceInitializer
-{
+public static class WorkspaceInitializer {
///
/// Returns whether the folder contains a .ykanban structure. Only the
/// passed folder is checked; parent folders are never searched.
@@ -25,16 +24,13 @@ public static class WorkspaceInitializer
/// The existing folder to initialize.
/// The folder does not exist on disk.
/// The workspace database already exists.
- public static void Initialize(string folderPath)
- {
- if (!Directory.Exists(folderPath))
- {
+ public static void Initialize(string folderPath) {
+ if (!Directory.Exists(folderPath)) {
throw new WorkspaceDirectoryMissingException(folderPath);
}
string databasePath = WorkspacePaths.Database(folderPath);
- if (File.Exists(databasePath))
- {
+ if (File.Exists(databasePath)) {
throw new InvalidOperationException($"The workspace database already exists: {databasePath}");
}
diff --git a/YKanBan/Storage/Workspace/WorkspaceLock.cs b/YKanBan/Storage/Workspace/WorkspaceLock.cs
index 8ac3753..1f59ce6 100644
--- a/YKanBan/Storage/Workspace/WorkspaceLock.cs
+++ b/YKanBan/Storage/Workspace/WorkspaceLock.cs
@@ -5,8 +5,7 @@ namespace YKanBan.Storage.Workspace;
/// held until disposal, so crashes and power loss release it automatically. The
/// lock file is kept empty; only its exclusive OS handle carries meaning.
///
-public sealed class WorkspaceLock : IDisposable
-{
+public sealed class WorkspaceLock : IDisposable {
private readonly FileStream _stream;
///
@@ -14,8 +13,7 @@ public sealed class WorkspaceLock : IDisposable
///
/// The exclusively opened lock file stream.
/// Path of the lock file being held.
- private WorkspaceLock(FileStream stream, string lockFilePath)
- {
+ private WorkspaceLock(FileStream stream, string lockFilePath) {
_stream = stream;
LockFilePath = lockFilePath;
}
@@ -32,24 +30,18 @@ public sealed class WorkspaceLock : IDisposable
/// The held lock; dispose it to release.
/// The folder has no .ykanban structure.
/// Another instance already holds the lock.
- public static WorkspaceLock Acquire(string folderPath)
- {
+ public static WorkspaceLock Acquire(string folderPath) {
string lockFilePath = WorkspacePaths.LockFile(folderPath);
FileStream stream;
- try
- {
+ try {
// FileShare.None gives the exclusive semantics; FileMode.Create empties any stale file.
// SAFETY: FileMode.Create truncates a pre-existing file, but the lock
// file is empty by design and the path always comes from
// WorkspacePaths.LockFile, so no user data is ever affected.
stream = new FileStream(lockFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
- }
- catch (DirectoryNotFoundException)
- {
+ } catch (DirectoryNotFoundException) {
throw new WorkspaceNotInitializedException(folderPath);
- }
- catch (IOException ex)
- {
+ } catch (IOException ex) {
throw new WorkspaceLockException(lockFilePath, ex);
}
diff --git a/YKanBan/Storage/Workspace/WorkspacePaths.cs b/YKanBan/Storage/Workspace/WorkspacePaths.cs
index 97e364e..0bd00be 100644
--- a/YKanBan/Storage/Workspace/WorkspacePaths.cs
+++ b/YKanBan/Storage/Workspace/WorkspacePaths.cs
@@ -4,8 +4,7 @@ namespace YKanBan.Storage.Workspace;
/// Well-known file and folder names inside a workspace's .ykanban structure,
/// plus helpers that combine them with a workspace folder path.
///
-public static class WorkspacePaths
-{
+public static class WorkspacePaths {
///
/// Name of the workspace root folder created inside a managed project.
///
diff --git a/YKanBan/Storage/Workspace/WorkspacePreset.cs b/YKanBan/Storage/Workspace/WorkspacePreset.cs
index 65c0a70..588eb90 100644
--- a/YKanBan/Storage/Workspace/WorkspacePreset.cs
+++ b/YKanBan/Storage/Workspace/WorkspacePreset.cs
@@ -8,16 +8,14 @@ namespace YKanBan.Storage.Workspace;
/// language active when they are added; they then become ordinary data that no
/// longer follows language switches.
///
-public static class WorkspacePreset
-{
+public static class WorkspacePreset {
///
/// Inserts the three preset columns (the current language's equivalents of
/// To Do / In Progress / Done) with their descriptions in a single transaction.
///
/// The initialized workspace folder.
/// The workspace database does not exist or a title collides with an existing column.
- public static void AddPresetColumns(string folderPath)
- {
+ public static void AddPresetColumns(string folderPath) {
using SqliteConnection connection = SqliteDatabase.Open(
WorkspacePaths.Database(folderPath), WorkspaceSchema.Migrations);
@@ -33,8 +31,7 @@ public static class WorkspacePreset
// Insert all three atomically so a partially populated workspace can never exist.
using var transaction = connection.BeginTransaction();
- foreach ((string title, string description) in columns)
- {
+ foreach ((string title, string description) in columns) {
using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = """
@@ -55,8 +52,7 @@ public static class WorkspacePreset
///
/// The initialized workspace folder.
/// The workspace database does not exist or a tag name collides.
- public static void AddPresetTags(string folderPath)
- {
+ public static void AddPresetTags(string folderPath) {
using SqliteConnection connection = SqliteDatabase.Open(
WorkspacePaths.Database(folderPath), WorkspaceSchema.Migrations);
@@ -74,8 +70,7 @@ public static class WorkspacePreset
];
using var transaction = connection.BeginTransaction();
- foreach ((string name, string color, string description) in tags)
- {
+ foreach ((string name, string color, string description) in tags) {
using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = """
diff --git a/YKanBan/Storage/Workspace/WorkspaceRepository.cs b/YKanBan/Storage/Workspace/WorkspaceRepository.cs
index c33de3e..9ef12fe 100644
--- a/YKanBan/Storage/Workspace/WorkspaceRepository.cs
+++ b/YKanBan/Storage/Workspace/WorkspaceRepository.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+
using YKanBan.Models;
using YKanBan.Search;
@@ -14,16 +15,14 @@ namespace YKanBan.Storage.Workspace;
/// - columns.updated_at refreshes only when title or description changes;
/// - a confirmation with no effective change performs no write at all.
///
-public sealed class WorkspaceRepository : IDisposable
-{
+public sealed class WorkspaceRepository : IDisposable {
private readonly SqliteConnection _connection;
///
/// Opens the repository over an initialized workspace database.
///
/// The workspace folder.
- public WorkspaceRepository(string folderPath)
- {
+ public WorkspaceRepository(string folderPath) {
_connection = SqliteDatabase.Open(WorkspacePaths.Database(folderPath), WorkspaceSchema.Migrations);
}
@@ -38,15 +37,13 @@ public sealed class WorkspaceRepository : IDisposable
/// Returns all columns ordered by id ascending.
///
/// All columns in id order.
- public IReadOnlyList GetColumns()
- {
+ public IReadOnlyList GetColumns() {
using var command = CreateCommand(
"SELECT id, title, description, created_at, updated_at FROM columns ORDER BY id ASC;");
using var reader = command.ExecuteReader();
var columns = new List();
- while (reader.Read())
- {
+ while (reader.Read()) {
columns.Add(ReadColumn(reader));
}
return columns;
@@ -57,8 +54,7 @@ public sealed class WorkspaceRepository : IDisposable
///
/// The column id.
/// The column, or when it does not exist.
- public ColumnModel? GetColumn(long columnId)
- {
+ public ColumnModel? GetColumn(long columnId) {
using var command = CreateCommand(
"SELECT id, title, description, created_at, updated_at FROM columns WHERE id = $id;");
command.Parameters.AddWithValue("$id", columnId);
@@ -73,8 +69,7 @@ public sealed class WorkspaceRepository : IDisposable
/// The optional description.
/// The created column.
/// The title is empty or not unique.
- public ColumnModel AddColumn(string title, string description)
- {
+ public ColumnModel AddColumn(string title, string description) {
long now = UnixTime.Now;
using var transaction = _connection.BeginTransaction();
long id = ExecuteInsert(transaction,
@@ -95,14 +90,12 @@ public sealed class WorkspaceRepository : IDisposable
/// The updated column, or the unchanged one for a no-op.
/// The column does not exist.
/// The new title is empty or not unique.
- public ColumnModel UpdateColumn(long columnId, string title, string description)
- {
+ public ColumnModel UpdateColumn(long columnId, string title, string description) {
ColumnModel existing = GetColumn(columnId)
?? throw new InvalidOperationException($"Column {columnId} does not exist.");
// Unchanged title and description: no write, no timestamp refresh.
- if (existing.Title == title && existing.Description == description)
- {
+ if (existing.Title == title && existing.Description == description) {
return existing;
}
@@ -133,8 +126,7 @@ public sealed class WorkspaceRepository : IDisposable
/// Returns all cards with their assigned tags loaded, ordered by id ascending.
///
/// All cards in id order.
- public IReadOnlyList GetCards()
- {
+ public IReadOnlyList GetCards() {
using var command = CreateCommand("""
SELECT c.id, c.column_id, c.title, c.content, c.created_at, c.updated_at,
t.id, t.name, t.color, t.description
@@ -148,11 +140,9 @@ public sealed class WorkspaceRepository : IDisposable
// Single grouped pass: one output card per distinct card id, with its tags accumulated alongside.
var cards = new List();
var tagsByCard = new Dictionary>();
- while (reader.Read())
- {
+ while (reader.Read()) {
long cardId = reader.GetInt64(0);
- if (!tagsByCard.TryGetValue(cardId, out List? tags))
- {
+ if (!tagsByCard.TryGetValue(cardId, out List? tags)) {
tags = new List();
tagsByCard[cardId] = tags;
cards.Add(new CardModel(
@@ -165,8 +155,7 @@ public sealed class WorkspaceRepository : IDisposable
tags));
}
- if (!reader.IsDBNull(6))
- {
+ if (!reader.IsDBNull(6)) {
tags.Add(ReadTag(reader, 6));
}
}
@@ -178,18 +167,15 @@ public sealed class WorkspaceRepository : IDisposable
///
/// The compiled search predicate and its parameters.
/// The matching card ids.
- public IReadOnlyCollection GetCardIdsMatching(CompiledSearch search)
- {
+ public IReadOnlyCollection GetCardIdsMatching(CompiledSearch search) {
using var command = CreateCommand($"SELECT c.id FROM cards c WHERE {search.Predicate};");
- foreach ((string name, object value) in search.Parameters)
- {
+ foreach ((string name, object value) in search.Parameters) {
command.Parameters.AddWithValue(name, value);
}
using var reader = command.ExecuteReader();
var ids = new HashSet();
- while (reader.Read())
- {
+ while (reader.Read()) {
ids.Add(reader.GetInt64(0));
}
return ids;
@@ -200,8 +186,7 @@ public sealed class WorkspaceRepository : IDisposable
///
/// The card id.
/// The card, or when it does not exist.
- public CardModel? GetCard(long cardId)
- {
+ public CardModel? GetCard(long cardId) {
using var command = CreateCommand("""
SELECT c.id, c.column_id, c.title, c.content, c.created_at, c.updated_at,
t.id, t.name, t.color, t.description
@@ -217,8 +202,7 @@ public sealed class WorkspaceRepository : IDisposable
// The LEFT JOIN may repeat the card row once per tag; build it on the first row only.
CardModel? card = null;
var tags = new List();
- while (reader.Read())
- {
+ while (reader.Read()) {
card ??= new CardModel(
reader.GetInt64(0),
reader.GetInt64(1),
@@ -228,8 +212,7 @@ public sealed class WorkspaceRepository : IDisposable
reader.GetInt64(5),
tags);
- if (!reader.IsDBNull(6))
- {
+ if (!reader.IsDBNull(6)) {
tags.Add(ReadTag(reader, 6));
}
}
@@ -245,15 +228,13 @@ public sealed class WorkspaceRepository : IDisposable
/// The tags to assign initially.
/// The created card with tags loaded.
/// The column or a tag id does not exist.
- public CardModel AddCard(long columnId, string title, string content, IReadOnlyCollection tagIds)
- {
+ public CardModel AddCard(long columnId, string title, string content, IReadOnlyCollection tagIds) {
long now = UnixTime.Now;
using var transaction = _connection.BeginTransaction();
long id = ExecuteInsert(transaction,
"INSERT INTO cards (column_id, title, content, created_at, updated_at) VALUES ($columnId, $title, $content, $now, $now);",
("$columnId", columnId), ("$title", title), ("$content", content), ("$now", now));
- foreach (long tagId in tagIds.Distinct())
- {
+ foreach (long tagId in tagIds.Distinct()) {
ExecuteNonQuery(transaction,
"INSERT INTO card_tags (card_id, tag_id) VALUES ($cardId, $tagId);",
("$cardId", id), ("$tagId", tagId));
@@ -276,15 +257,13 @@ public sealed class WorkspaceRepository : IDisposable
/// The updated card with tags loaded, or the unchanged one for a no-op.
/// The card does not exist.
/// A tag id does not exist; nothing is written.
- public CardModel UpdateCard(long cardId, string title, string content, IReadOnlyCollection tagIds)
- {
+ public CardModel UpdateCard(long cardId, string title, string content, IReadOnlyCollection tagIds) {
CardModel existing = GetCard(cardId)
?? throw new InvalidOperationException($"Card {cardId} does not exist.");
// No effective change in title, content or tag set: no write, no timestamp refresh.
bool tagsChanged = !tagIds.Distinct().ToHashSet().SetEquals(existing.Tags.Select(tag => tag.Id));
- if (existing.Title == title && existing.Content == content && !tagsChanged)
- {
+ if (existing.Title == title && existing.Content == content && !tagsChanged) {
return existing;
}
@@ -297,8 +276,7 @@ public sealed class WorkspaceRepository : IDisposable
// Tag assignment is rewritten wholesale; card_tags carries nothing beyond the pair.
ExecuteNonQuery(transaction, "DELETE FROM card_tags WHERE card_id = $id;", ("$id", cardId));
- foreach (long tagId in tagIds.Distinct())
- {
+ foreach (long tagId in tagIds.Distinct()) {
ExecuteNonQuery(transaction,
"INSERT INTO card_tags (card_id, tag_id) VALUES ($cardId, $tagId);",
("$cardId", cardId), ("$tagId", tagId));
@@ -316,8 +294,7 @@ public sealed class WorkspaceRepository : IDisposable
/// The card to move.
/// The destination column.
/// when the card actually moved.
- public bool MoveCard(long cardId, long targetColumnId)
- {
+ public bool MoveCard(long cardId, long targetColumnId) {
using var command = CreateCommand(
"UPDATE cards SET column_id = $columnId WHERE id = $id AND column_id <> $columnId;");
command.Parameters.AddWithValue("$columnId", targetColumnId);
@@ -340,15 +317,13 @@ public sealed class WorkspaceRepository : IDisposable
/// Returns all tags ordered by id ascending; unreferenced tags are included.
///
/// All tags in id order.
- public IReadOnlyList GetTags()
- {
+ public IReadOnlyList GetTags() {
using var command = CreateCommand(
"SELECT id, name, color, description FROM tags ORDER BY id ASC;");
using var reader = command.ExecuteReader();
var tags = new List();
- while (reader.Read())
- {
+ while (reader.Read()) {
tags.Add(ReadTag(reader, 0));
}
return tags;
@@ -358,8 +333,7 @@ public sealed class WorkspaceRepository : IDisposable
/// Returns every tag together with the number of cards using it, ordered by tag id.
///
/// The tag/usage pairs in tag id order.
- public IReadOnlyList<(TagModel Tag, long UsageCount)> GetTagsWithUsage()
- {
+ public IReadOnlyList<(TagModel Tag, long UsageCount)> GetTagsWithUsage() {
using var command = CreateCommand("""
SELECT t.id, t.name, t.color, t.description, COUNT(ct.card_id)
FROM tags t
@@ -370,8 +344,7 @@ public sealed class WorkspaceRepository : IDisposable
using var reader = command.ExecuteReader();
var tags = new List<(TagModel Tag, long UsageCount)>();
- while (reader.Read())
- {
+ while (reader.Read()) {
tags.Add((ReadTag(reader, 0), reader.GetInt64(4)));
}
return tags;
@@ -385,8 +358,7 @@ public sealed class WorkspaceRepository : IDisposable
/// The optional description.
/// The created tag.
/// The name is empty or not unique.
- public TagModel AddTag(string name, RgbColor color, string description)
- {
+ public TagModel AddTag(string name, RgbColor color, string description) {
using var transaction = _connection.BeginTransaction();
long id = ExecuteInsert(transaction,
"INSERT INTO tags (name, color, description) VALUES ($name, $color, $description);",
@@ -406,14 +378,12 @@ public sealed class WorkspaceRepository : IDisposable
/// The updated tag, or the unchanged one for a no-op.
/// The tag does not exist.
/// The new name is empty or not unique.
- public TagModel UpdateTag(long tagId, string name, RgbColor color, string description)
- {
+ public TagModel UpdateTag(long tagId, string name, RgbColor color, string description) {
TagModel existing = GetTag(tagId)
?? throw new InvalidOperationException($"Tag {tagId} does not exist.");
// Unchanged name, color and description: no write.
- if (existing.Name == name && existing.Color == color && existing.Description == description)
- {
+ if (existing.Name == name && existing.Color == color && existing.Description == description) {
return existing;
}
@@ -444,8 +414,7 @@ public sealed class WorkspaceRepository : IDisposable
///
/// The tag id.
/// The tag, or when it does not exist.
- private TagModel? GetTag(long tagId)
- {
+ private TagModel? GetTag(long tagId) {
using var command = CreateCommand(
"SELECT id, name, color, description FROM tags WHERE id = $id;");
command.Parameters.AddWithValue("$id", tagId);
@@ -458,8 +427,7 @@ public sealed class WorkspaceRepository : IDisposable
///
/// The card id.
/// The assigned tags in id order.
- private IReadOnlyList LoadTags(long cardId)
- {
+ private IReadOnlyList LoadTags(long cardId) {
using var command = CreateCommand("""
SELECT t.id, t.name, t.color, t.description
FROM tags t
@@ -471,8 +439,7 @@ public sealed class WorkspaceRepository : IDisposable
using var reader = command.ExecuteReader();
var tags = new List();
- while (reader.Read())
- {
+ while (reader.Read()) {
tags.Add(ReadTag(reader, 0));
}
return tags;
@@ -502,8 +469,7 @@ public sealed class WorkspaceRepository : IDisposable
/// The command text.
/// The transaction to enlist in, when any.
/// The prepared command.
- private SqliteCommand CreateCommand(string sql, SqliteTransaction? transaction = null)
- {
+ private SqliteCommand CreateCommand(string sql, SqliteTransaction? transaction = null) {
SqliteCommand command = _connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = sql;
@@ -516,11 +482,9 @@ public sealed class WorkspaceRepository : IDisposable
/// The transaction to enlist in.
/// The statement to execute.
/// Name/value pairs bound as SQL parameters.
- private void ExecuteNonQuery(SqliteTransaction transaction, string sql, params (string Name, object? Value)[] parameters)
- {
+ private void ExecuteNonQuery(SqliteTransaction transaction, string sql, params (string Name, object? Value)[] parameters) {
using SqliteCommand command = CreateCommand(sql, transaction);
- foreach ((string name, object? value) in parameters)
- {
+ foreach ((string name, object? value) in parameters) {
command.Parameters.AddWithValue(name, value);
}
command.ExecuteNonQuery();
@@ -533,8 +497,7 @@ public sealed class WorkspaceRepository : IDisposable
/// The INSERT statement.
/// Name/value pairs bound as SQL parameters.
/// The rowid assigned to the inserted row.
- private long ExecuteInsert(SqliteTransaction transaction, string sql, params (string Name, object? Value)[] parameters)
- {
+ private long ExecuteInsert(SqliteTransaction transaction, string sql, params (string Name, object? Value)[] parameters) {
ExecuteNonQuery(transaction, sql, parameters);
using SqliteCommand rowId = CreateCommand("SELECT last_insert_rowid();", transaction);
return (long)rowId.ExecuteScalar()!;
@@ -546,8 +509,7 @@ public sealed class WorkspaceRepository : IDisposable
/// The DELETE statement with an $id parameter.
/// The id to delete.
/// when a row was deleted.
- private bool ExecuteDelete(string sql, long id)
- {
+ private bool ExecuteDelete(string sql, long id) {
using SqliteCommand command = CreateCommand(sql);
command.Parameters.AddWithValue("$id", id);
return command.ExecuteNonQuery() > 0;
diff --git a/YKanBan/Storage/Workspace/WorkspaceSchema.cs b/YKanBan/Storage/Workspace/WorkspaceSchema.cs
index 4e91e22..02e59ba 100644
--- a/YKanBan/Storage/Workspace/WorkspaceSchema.cs
+++ b/YKanBan/Storage/Workspace/WorkspaceSchema.cs
@@ -7,8 +7,7 @@ namespace YKanBan.Storage.Workspace;
/// KEY) so every stored row is valid by construction, and cascading deletes
/// are covered by the same DDL.
///
-public static class WorkspaceSchema
-{
+public static class WorkspaceSchema {
///
/// Newest workspace schema version this build understands.
///
diff --git a/YKanBan/Storage/Workspace/WorkspaceSession.cs b/YKanBan/Storage/Workspace/WorkspaceSession.cs
index 4d11333..1a6cc70 100644
--- a/YKanBan/Storage/Workspace/WorkspaceSession.cs
+++ b/YKanBan/Storage/Workspace/WorkspaceSession.cs
@@ -7,8 +7,7 @@ namespace YKanBan.Storage.Workspace;
/// any database work happens. Disposing releases the repository and then the
/// lock.
///
-public sealed class WorkspaceSession : IDisposable
-{
+public sealed class WorkspaceSession : IDisposable {
private readonly WorkspaceLock _workspaceLock;
///
@@ -17,8 +16,7 @@ public sealed class WorkspaceSession : IDisposable
/// The canonical workspace folder path.
/// The exclusive lock held while this session is open.
/// The data access object for this workspace.
- private WorkspaceSession(string folderPath, WorkspaceLock workspaceLock, WorkspaceRepository repository)
- {
+ private WorkspaceSession(string folderPath, WorkspaceLock workspaceLock, WorkspaceRepository repository) {
FolderPath = folderPath;
_workspaceLock = workspaceLock;
Repository = repository;
@@ -41,16 +39,12 @@ public sealed class WorkspaceSession : IDisposable
/// The opened session; dispose it to release the lock and connection.
/// The folder has no .ykanban structure.
/// Another instance already holds the lock.
- public static WorkspaceSession Open(string folderPath)
- {
+ public static WorkspaceSession Open(string folderPath) {
WorkspaceLock workspaceLock = WorkspaceLock.Acquire(folderPath);
- try
- {
+ try {
var repository = new WorkspaceRepository(folderPath);
return new WorkspaceSession(folderPath, workspaceLock, repository);
- }
- catch
- {
+ } catch {
workspaceLock.Dispose();
throw;
}
@@ -59,8 +53,7 @@ public sealed class WorkspaceSession : IDisposable
///
/// Releases the repository connection, then the exclusive lock.
///
- public void Dispose()
- {
+ public void Dispose() {
Repository.Dispose();
_workspaceLock.Dispose();
}
diff --git a/YKanBan/ViewLocator.cs b/YKanBan/ViewLocator.cs
index 651f07e..d1b237e 100644
--- a/YKanBan/ViewLocator.cs
+++ b/YKanBan/ViewLocator.cs
@@ -1,31 +1,29 @@
using System;
+
using Avalonia.Controls;
using Avalonia.Controls.Templates;
+
using YKanBan.ViewModels;
namespace YKanBan;
-public class ViewLocator : IDataTemplate
-{
+public class ViewLocator : IDataTemplate {
- public Control? Build(object? param)
- {
+ public Control? Build(object? param) {
if (param is null)
return null;
-
+
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
- if (type != null)
- {
+ if (type != null) {
return (Control)Activator.CreateInstance(type)!;
}
-
+
return new TextBlock { Text = "Not Found: " + name };
}
- public bool Match(object? data)
- {
+ public bool Match(object? data) {
return data is ViewModelBase;
}
}
diff --git a/YKanBan/ViewModels/Board/BoardViewModel.cs b/YKanBan/ViewModels/Board/BoardViewModel.cs
index 88b69db..8cf166b 100644
--- a/YKanBan/ViewModels/Board/BoardViewModel.cs
+++ b/YKanBan/ViewModels/Board/BoardViewModel.cs
@@ -1,6 +1,8 @@
using System.Collections.ObjectModel;
+
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+
using YKanBan.Models;
using YKanBan.Search;
using YKanBan.Services;
@@ -15,8 +17,7 @@ namespace YKanBan.ViewModels.Board;
/// persistence goes through the workspace repository; the board reloads from it
/// after each change.
///
-public sealed partial class BoardViewModel : ViewModelBase
-{
+public sealed partial class BoardViewModel : ViewModelBase {
private readonly AppServices _services;
private readonly WorkspaceRepository _repository;
@@ -28,8 +29,7 @@ public sealed partial class BoardViewModel : ViewModelBase
///
/// The process composition root (configuration access).
/// The open workspace session.
- public BoardViewModel(AppServices services, WorkspaceSession session)
- {
+ public BoardViewModel(AppServices services, WorkspaceSession session) {
_services = services;
_repository = session.Repository;
Display = new DisplaySettingsViewModel(_services.Config, Reload);
@@ -97,28 +97,44 @@ public sealed partial class BoardViewModel : ViewModelBase
#region Interactions
- /// Gets the interaction that opens the new-column editor.
+ ///
+ /// Gets the interaction that opens the new-column editor.
+ ///
public Interaction AddColumn { get; } = new();
- /// Gets the interaction that opens the column editor.
+ ///
+ /// Gets the interaction that opens the column editor.
+ ///
public Interaction EditColumn { get; } = new();
- /// Gets the interaction that confirms and deletes a column.
+ ///
+ /// Gets the interaction that confirms and deletes a column.
+ ///
public Interaction DeleteColumn { get; } = new();
- /// Gets the interaction that opens the new-card editor.
+ ///
+ /// Gets the interaction that opens the new-card editor.
+ ///
public Interaction AddCard { get; } = new();
- /// Gets the interaction that opens the card editor.
+ ///
+ /// Gets the interaction that opens the card editor.
+ ///
public Interaction EditCard { get; } = new();
- /// Gets the interaction that confirms and deletes a card.
+ ///
+ /// Gets the interaction that confirms and deletes a card.
+ ///
public Interaction DeleteCard { get; } = new();
- /// Gets the interaction that opens the move-card dialog.
+ ///
+ /// Gets the interaction that opens the move-card dialog.
+ ///
public Interaction MoveCard { get; } = new();
- /// Gets the interaction that shows the search error dialog, carrying the English parser detail.
+ ///
+ /// Gets the interaction that shows the search error dialog, carrying the English parser detail.
+ ///
public Interaction ShowSearchError { get; } = new();
#endregion
@@ -136,28 +152,22 @@ public sealed partial class BoardViewModel : ViewModelBase
/// expression opens the error dialog and keeps the previous results and text.
///
[RelayCommand]
- private async Task SearchAsync()
- {
- if (string.IsNullOrWhiteSpace(SearchText))
- {
+ private async Task SearchAsync() {
+ if (string.IsNullOrWhiteSpace(SearchText)) {
ApplyFilter(null);
return;
}
- try
- {
+ try {
SearchNode? query = SearchQueryParser.Parse(SearchText);
- if (query is null)
- {
+ if (query is null) {
ApplyFilter(null);
return;
}
CompiledSearch compiled = SearchSqlCompiler.Compile(query);
ApplyFilter(_repository.GetCardIdsMatching(compiled));
- }
- catch (SearchSyntaxException exception)
- {
+ } catch (SearchSyntaxException exception) {
// The error dialog carries the parser's non-localized English detail.
await ShowSearchError.Handle(exception.Message);
}
@@ -167,8 +177,7 @@ public sealed partial class BoardViewModel : ViewModelBase
/// Clears the search box and restores the unfiltered board.
///
[RelayCommand]
- private void ClearSearch()
- {
+ private void ClearSearch() {
SearchText = string.Empty;
ApplyFilter(null);
}
@@ -177,10 +186,8 @@ public sealed partial class BoardViewModel : ViewModelBase
/// Applies a new result filter (null clears it) and rebuilds the board when it changed.
///
/// The new filter, or for no filter.
- private void ApplyFilter(IReadOnlyCollection? matchedIds)
- {
- if (_matchedCardIds is null && matchedIds is null)
- {
+ private void ApplyFilter(IReadOnlyCollection? matchedIds) {
+ if (_matchedCardIds is null && matchedIds is null) {
return;
}
@@ -244,14 +251,12 @@ public sealed partial class BoardViewModel : ViewModelBase
///
/// Reloads all columns and cards from the repository and rebuilds the board.
///
- public void Reload()
- {
+ public void Reload() {
IReadOnlyList columns = _repository.GetColumns();
IReadOnlyList cards = _repository.GetCards();
// An active search hides non-matching cards while every column stays visible.
- if (_matchedCardIds is not null)
- {
+ if (_matchedCardIds is not null) {
cards = cards.Where(card => _matchedCardIds.Contains(card.Id)).ToList();
}
@@ -263,8 +268,7 @@ public sealed partial class BoardViewModel : ViewModelBase
IEnumerable orderedColumns = SortColumns(columns, _services.Config.Display.ColumnSort);
Columns.Clear();
- foreach (ColumnModel column in orderedColumns)
- {
+ foreach (ColumnModel column in orderedColumns) {
// Cards are sorted within their column by the selected card order.
IEnumerable columnCards = cardsByColumn.TryGetValue(column.Id, out List? list)
? SortCards(list, _services.Config.Display.CardSort)
@@ -280,11 +284,9 @@ public sealed partial class BoardViewModel : ViewModelBase
///
/// Recomputes the global column width and pushes it to every column.
///
- public void ApplyColumnWidth()
- {
+ public void ApplyColumnWidth() {
ColumnWidth = ColumnWidthPixels(_services.Config.Display.ColumnWidth);
- foreach (ColumnViewModel column in Columns)
- {
+ foreach (ColumnViewModel column in Columns) {
column.Width = ColumnWidth;
}
}
@@ -294,8 +296,7 @@ public sealed partial class BoardViewModel : ViewModelBase
///
/// The configured column width.
/// The width in pixels.
- public static double ColumnWidthPixels(ColumnWidthSettings settings) => settings.Preset switch
- {
+ public static double ColumnWidthPixels(ColumnWidthSettings settings) => settings.Preset switch {
ColumnWidthPreset.Narrow => 240,
ColumnWidthPreset.Wide => 480,
ColumnWidthPreset.Custom => settings.CustomPixels,
@@ -308,8 +309,7 @@ public sealed partial class BoardViewModel : ViewModelBase
/// The cards to order.
/// The selected order.
/// The ordered cards.
- private static IEnumerable SortCards(IEnumerable cards, SortOption option) => option switch
- {
+ private static IEnumerable SortCards(IEnumerable cards, SortOption option) => option switch {
SortOption.IdAscending => cards.OrderBy(card => card.Id),
SortOption.IdDescending => cards.OrderByDescending(card => card.Id),
SortOption.CreatedAscending => cards.OrderBy(card => card.CreatedAt).ThenBy(card => card.Id),
@@ -327,8 +327,7 @@ public sealed partial class BoardViewModel : ViewModelBase
/// The columns to order.
/// The selected order.
/// The ordered columns.
- private static IEnumerable SortColumns(IEnumerable columns, SortOption option) => option switch
- {
+ private static IEnumerable SortColumns(IEnumerable columns, SortOption option) => option switch {
SortOption.IdAscending => columns.OrderBy(column => column.Id),
SortOption.IdDescending => columns.OrderByDescending(column => column.Id),
SortOption.CreatedAscending => columns.OrderBy(column => column.CreatedAt).ThenBy(column => column.Id),
diff --git a/YKanBan/ViewModels/Board/CardViewModel.cs b/YKanBan/ViewModels/Board/CardViewModel.cs
index e54863c..53ec052 100644
--- a/YKanBan/ViewModels/Board/CardViewModel.cs
+++ b/YKanBan/ViewModels/Board/CardViewModel.cs
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.Input;
+
using YKanBan.Models;
namespace YKanBan.ViewModels.Board;
@@ -9,9 +10,10 @@ namespace YKanBan.ViewModels.Board;
/// truncated content preview and the colored tag badges; timestamps are not
/// shown on the board.
///
-public sealed partial class CardViewModel : ViewModelBase
-{
- /// Maximum number of content characters shown in the card preview.
+public sealed partial class CardViewModel : ViewModelBase {
+ ///
+ /// Maximum number of content characters shown in the card preview.
+ ///
private const int PreviewLength = 200;
private readonly BoardViewModel _board;
@@ -21,8 +23,7 @@ public sealed partial class CardViewModel : ViewModelBase
///
/// The owning board view model.
/// The card model to display.
- public CardViewModel(BoardViewModel board, CardModel card)
- {
+ public CardViewModel(BoardViewModel board, CardModel card) {
_board = board;
Model = card;
TagBadges = card.Tags.Select(tag => new TagBadgeViewModel(tag)).ToList();
@@ -71,10 +72,8 @@ public sealed partial class CardViewModel : ViewModelBase
///
/// Gets the content preview, collapsed to one line and truncated.
///
- public string PreviewContent
- {
- get
- {
+ public string PreviewContent {
+ get {
string singleLine = Model.Content.Replace("\r", " ").Replace("\n", " ");
return singleLine.Length <= PreviewLength ? singleLine : singleLine[..PreviewLength] + "…";
}
diff --git a/YKanBan/ViewModels/Board/ColumnViewModel.cs b/YKanBan/ViewModels/Board/ColumnViewModel.cs
index 123bd07..cc56872 100644
--- a/YKanBan/ViewModels/Board/ColumnViewModel.cs
+++ b/YKanBan/ViewModels/Board/ColumnViewModel.cs
@@ -1,6 +1,8 @@
using System.Collections.ObjectModel;
+
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+
using YKanBan.Models;
namespace YKanBan.ViewModels.Board;
@@ -9,8 +11,7 @@ namespace YKanBan.ViewModels.Board;
/// One board column: a header with its title, card count and actions, plus the
/// vertically stacked cards.
///
-public sealed partial class ColumnViewModel : ViewModelBase
-{
+public sealed partial class ColumnViewModel : ViewModelBase {
private readonly BoardViewModel _board;
///
@@ -19,8 +20,7 @@ public sealed partial class ColumnViewModel : ViewModelBase
/// The owning board view model.
/// The column model.
/// The already sorted cards belonging to this column.
- public ColumnViewModel(BoardViewModel board, ColumnModel column, IEnumerable cards)
- {
+ public ColumnViewModel(BoardViewModel board, ColumnModel column, IEnumerable cards) {
_board = board;
Model = column;
Cards = new ObservableCollection(cards);
diff --git a/YKanBan/ViewModels/Board/DisplaySettingsViewModel.cs b/YKanBan/ViewModels/Board/DisplaySettingsViewModel.cs
index 6eb882e..c6b42a0 100644
--- a/YKanBan/ViewModels/Board/DisplaySettingsViewModel.cs
+++ b/YKanBan/ViewModels/Board/DisplaySettingsViewModel.cs
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
+
using YKanBan.Storage.AppData;
namespace YKanBan.ViewModels.Board;
@@ -8,12 +9,15 @@ namespace YKanBan.ViewModels.Board;
/// width and the separate card and column orderings. Every change writes to
/// the configuration and asks the board to refresh immediately.
///
-public sealed partial class DisplaySettingsViewModel : ViewModelBase
-{
- /// Smallest accepted manual width.
+public sealed partial class DisplaySettingsViewModel : ViewModelBase {
+ ///
+ /// Smallest accepted manual width.
+ ///
public const int MinimumPixels = 160;
- /// Largest accepted manual width.
+ ///
+ /// Largest accepted manual width.
+ ///
public const int MaximumPixels = 720;
private readonly AppConfig _config;
@@ -24,8 +28,7 @@ public sealed partial class DisplaySettingsViewModel : ViewModelBase
///
/// The live application configuration.
/// Invoked after every change to refresh the board.
- public DisplaySettingsViewModel(AppConfig config, Action refresh)
- {
+ public DisplaySettingsViewModel(AppConfig config, Action refresh) {
_config = config;
_refresh = refresh;
@@ -88,11 +91,9 @@ public sealed partial class DisplaySettingsViewModel : ViewModelBase
/// Stores the width preset and refreshes the board.
///
/// The new selection.
- partial void OnSelectedPresetChanged(OptionItem? value)
- {
+ partial void OnSelectedPresetChanged(OptionItem? value) {
OnPropertyChanged(nameof(IsCustom));
- if (value is not null)
- {
+ if (value is not null) {
_config.Display.ColumnWidth.Preset = (ColumnWidthPreset)value.Value;
_refresh();
}
@@ -102,17 +103,14 @@ public sealed partial class DisplaySettingsViewModel : ViewModelBase
/// Stores the manual width and refreshes the board when the custom preset is active.
///
/// The new pixel count.
- partial void OnCustomPixelsChanged(decimal? value)
- {
- if (value is null)
- {
+ partial void OnCustomPixelsChanged(decimal? value) {
+ if (value is null) {
return;
}
int pixels = (int)value.Value;
_config.Display.ColumnWidth.CustomPixels = pixels;
- if (IsCustom)
- {
+ if (IsCustom) {
_refresh();
}
}
@@ -121,10 +119,8 @@ public sealed partial class DisplaySettingsViewModel : ViewModelBase
/// Stores the card order and refreshes the board.
///
/// The new selection.
- partial void OnSelectedCardSortChanged(OptionItem? value)
- {
- if (value is not null)
- {
+ partial void OnSelectedCardSortChanged(OptionItem? value) {
+ if (value is not null) {
_config.Display.CardSort = (SortOption)value.Value;
_refresh();
}
@@ -134,10 +130,8 @@ public sealed partial class DisplaySettingsViewModel : ViewModelBase
/// Stores the column order and refreshes the board.
///
/// The new selection.
- partial void OnSelectedColumnSortChanged(OptionItem? value)
- {
- if (value is not null)
- {
+ partial void OnSelectedColumnSortChanged(OptionItem? value) {
+ if (value is not null) {
_config.Display.ColumnSort = (SortOption)value.Value;
_refresh();
}
diff --git a/YKanBan/ViewModels/Board/TagBadgeViewModel.cs b/YKanBan/ViewModels/Board/TagBadgeViewModel.cs
index 2f8f06e..7a6309b 100644
--- a/YKanBan/ViewModels/Board/TagBadgeViewModel.cs
+++ b/YKanBan/ViewModels/Board/TagBadgeViewModel.cs
@@ -1,4 +1,5 @@
using Avalonia.Media;
+
using YKanBan.Models;
namespace YKanBan.ViewModels.Board;
@@ -8,14 +9,12 @@ namespace YKanBan.ViewModels.Board;
/// background taken directly from the tag color (user data, never themed) and
/// an automatically contrasting text color.
///
-public sealed class TagBadgeViewModel
-{
+public sealed class TagBadgeViewModel {
///
/// Initializes a badge from a tag.
///
/// The tag to render.
- public TagBadgeViewModel(TagModel tag)
- {
+ public TagBadgeViewModel(TagModel tag) {
Tag = tag;
Name = tag.Name;
Color background = Color.FromRgb(tag.Color.R, tag.Color.G, tag.Color.B);
@@ -54,8 +53,7 @@ public sealed class TagBadgeViewModel
///
/// The badge background color.
/// The contrasting text color.
- private static Color ContrastingTextColor(Color background)
- {
+ private static Color ContrastingTextColor(Color background) {
double luminance = (0.299 * background.R + 0.587 * background.G + 0.114 * background.B) / 255.0;
return luminance > 0.6 ? Colors.Black : Colors.White;
}
diff --git a/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs b/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs
index 48bfc53..234bc2e 100644
--- a/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/AboutDialogViewModel.cs
@@ -5,13 +5,11 @@ namespace YKanBan.ViewModels.Dialogs;
///
/// The about dialog: application name, version, repository link and license.
///
-public sealed class AboutDialogViewModel : ViewModelBase
-{
+public sealed class AboutDialogViewModel : ViewModelBase {
///
/// Initializes the view model, reading the version from the assembly.
///
- public AboutDialogViewModel()
- {
+ public AboutDialogViewModel() {
string version = typeof(AboutDialogViewModel).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
Version = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture, Resources.About_Version, version);
diff --git a/YKanBan/ViewModels/Dialogs/AssignedTagViewModel.cs b/YKanBan/ViewModels/Dialogs/AssignedTagViewModel.cs
index a640b89..f545f98 100644
--- a/YKanBan/ViewModels/Dialogs/AssignedTagViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/AssignedTagViewModel.cs
@@ -1,5 +1,7 @@
using Avalonia.Media;
+
using CommunityToolkit.Mvvm.Input;
+
using YKanBan.Models;
using YKanBan.ViewModels.Board;
@@ -10,8 +12,7 @@ namespace YKanBan.ViewModels.Dialogs;
/// remove action that only changes the in-memory assignment (the actual write
/// happens on confirm).
///
-public sealed partial class AssignedTagViewModel
-{
+public sealed partial class AssignedTagViewModel {
private readonly CardEditorViewModel _editor;
private readonly TagBadgeViewModel _badge;
@@ -20,8 +21,7 @@ public sealed partial class AssignedTagViewModel
///
/// The owning card editor.
/// The assigned tag.
- public AssignedTagViewModel(CardEditorViewModel editor, TagModel tag)
- {
+ public AssignedTagViewModel(CardEditorViewModel editor, TagModel tag) {
_editor = editor;
Tag = tag;
_badge = new TagBadgeViewModel(tag);
diff --git a/YKanBan/ViewModels/Dialogs/CardEditorViewModel.cs b/YKanBan/ViewModels/Dialogs/CardEditorViewModel.cs
index e5856da..3761753 100644
--- a/YKanBan/ViewModels/Dialogs/CardEditorViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/CardEditorViewModel.cs
@@ -1,8 +1,11 @@
using System.Collections.ObjectModel;
using System.Globalization;
+
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
+
using Microsoft.Data.Sqlite;
+
using YKanBan.Models;
using YKanBan.Storage.Workspace;
@@ -14,8 +17,7 @@ namespace YKanBan.ViewModels.Dialogs;
/// card and its tag assignment as one unit; a confirm with no effective change
/// writes nothing; cancelling never writes.
///
-public sealed partial class CardEditorViewModel : ViewModelBase
-{
+public sealed partial class CardEditorViewModel : ViewModelBase {
private readonly WorkspaceRepository _repository;
private readonly long _columnId;
private readonly CardModel? _existing;
@@ -26,8 +28,7 @@ public sealed partial class CardEditorViewModel : ViewModelBase
/// The workspace repository.
/// The owning column (used when adding).
/// The card to edit, or to add one.
- public CardEditorViewModel(WorkspaceRepository repository, long columnId, CardModel? existing)
- {
+ public CardEditorViewModel(WorkspaceRepository repository, long columnId, CardModel? existing) {
_repository = repository;
_columnId = columnId;
_existing = existing;
@@ -36,8 +37,7 @@ public sealed partial class CardEditorViewModel : ViewModelBase
AssignedTags = new ObservableCollection(
(existing?.Tags ?? []).Select(tag => new AssignedTagViewModel(this, tag)));
- if (existing is not null)
- {
+ if (existing is not null) {
IdLine = string.Format(Culture, Resources.CardDialog_Id, $"#{existing.Id}");
CreatedLine = string.Format(Culture, Resources.CardDialog_Created, FormatTime(existing.CreatedAt));
ModifiedLine = string.Format(Culture, Resources.CardDialog_Modified, FormatTime(existing.UpdatedAt));
@@ -105,12 +105,9 @@ public sealed partial class CardEditorViewModel : ViewModelBase
///
/// Gets a value indicating whether there are unsaved effective changes.
///
- public bool HasChanges
- {
- get
- {
- if (_existing is null)
- {
+ public bool HasChanges {
+ get {
+ if (_existing is null) {
return Title.Length > 0 || Content.Length > 0 || AssignedTags.Count > 0;
}
@@ -124,11 +121,9 @@ public sealed partial class CardEditorViewModel : ViewModelBase
/// Opens the nested tag picker and adds the chosen tag when it is new to the card.
///
[RelayCommand]
- private async Task AddTagAsync()
- {
+ private async Task AddTagAsync() {
TagModel? tag = await ShowTagPicker.Handle(Unit.Value);
- if (tag is not null && AssignedTags.All(row => row.Tag.Id != tag.Id))
- {
+ if (tag is not null && AssignedTags.All(row => row.Tag.Id != tag.Id)) {
AssignedTags.Add(new AssignedTagViewModel(this, tag));
}
}
@@ -137,11 +132,9 @@ public sealed partial class CardEditorViewModel : ViewModelBase
/// Removes a tag from the in-memory assignment.
///
/// The tag to remove.
- public void RemoveTag(TagModel tag)
- {
+ public void RemoveTag(TagModel tag) {
AssignedTagViewModel? row = AssignedTags.FirstOrDefault(item => item.Tag.Id == tag.Id);
- if (row is not null)
- {
+ if (row is not null) {
AssignedTags.Remove(row);
}
}
@@ -150,32 +143,24 @@ public sealed partial class CardEditorViewModel : ViewModelBase
/// Writes the card and its tags when there is an effective change.
///
/// when the dialog may close.
- public bool TryAccept()
- {
+ public bool TryAccept() {
ErrorMessage = null;
// A confirm with no effective change writes nothing and just closes.
- if (!HasChanges)
- {
+ if (!HasChanges) {
return true;
}
- try
- {
+ try {
IReadOnlyCollection tagIds = AssignedTags.Select(row => row.Tag.Id).ToList();
- if (_existing is null)
- {
+ if (_existing is null) {
_repository.AddCard(_columnId, Title, Content, tagIds);
- }
- else
- {
+ } else {
_repository.UpdateCard(_existing.Id, Title, Content, tagIds);
}
return true;
- }
- catch (SqliteException)
- {
+ } catch (SqliteException) {
ErrorMessage = Resources.CardDialog_Error;
return false;
}
diff --git a/YKanBan/ViewModels/Dialogs/ColumnEditorViewModel.cs b/YKanBan/ViewModels/Dialogs/ColumnEditorViewModel.cs
index f5d5438..9ff5b08 100644
--- a/YKanBan/ViewModels/Dialogs/ColumnEditorViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/ColumnEditorViewModel.cs
@@ -1,5 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
+
using Microsoft.Data.Sqlite;
+
using YKanBan.Models;
using YKanBan.Storage.Workspace;
@@ -10,8 +12,7 @@ namespace YKanBan.ViewModels.Dialogs;
/// The write happens when the dialog is accepted; a duplicate or empty title
/// keeps the dialog open with an inline error.
///
-public sealed partial class ColumnEditorViewModel : ViewModelBase
-{
+public sealed partial class ColumnEditorViewModel : ViewModelBase {
private readonly WorkspaceRepository _repository;
private readonly ColumnModel? _existing;
@@ -20,8 +21,7 @@ public sealed partial class ColumnEditorViewModel : ViewModelBase
///
/// The workspace repository.
/// The column to edit, or to add one.
- public ColumnEditorViewModel(WorkspaceRepository repository, ColumnModel? existing)
- {
+ public ColumnEditorViewModel(WorkspaceRepository repository, ColumnModel? existing) {
_repository = repository;
_existing = existing;
title = existing?.Title ?? string.Empty;
@@ -60,24 +60,17 @@ public sealed partial class ColumnEditorViewModel : ViewModelBase
/// Writes the column when accepted.
///
/// when the dialog may close.
- public bool TryAccept()
- {
+ public bool TryAccept() {
ErrorMessage = null;
- try
- {
- if (_existing is null)
- {
+ try {
+ if (_existing is null) {
_repository.AddColumn(Title, Description);
- }
- else
- {
+ } else {
_repository.UpdateColumn(_existing.Id, Title, Description);
}
return true;
- }
- catch (SqliteException)
- {
+ } catch (SqliteException) {
ErrorMessage = Resources.ColumnDialog_InvalidTitle;
return false;
}
diff --git a/YKanBan/ViewModels/Dialogs/DeleteCardConfirmViewModel.cs b/YKanBan/ViewModels/Dialogs/DeleteCardConfirmViewModel.cs
index dfb4d0a..97f72e5 100644
--- a/YKanBan/ViewModels/Dialogs/DeleteCardConfirmViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/DeleteCardConfirmViewModel.cs
@@ -5,14 +5,12 @@ namespace YKanBan.ViewModels.Dialogs;
///
/// The confirmation shown before deleting a card, naming the card id.
///
-public sealed class DeleteCardConfirmViewModel : ViewModelBase
-{
+public sealed class DeleteCardConfirmViewModel : ViewModelBase {
///
/// Initializes the confirmation.
///
/// The id of the card to delete.
- public DeleteCardConfirmViewModel(long cardId)
- {
+ public DeleteCardConfirmViewModel(long cardId) {
Message = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture,
Resources.Confirm_DeleteCard_Message,
diff --git a/YKanBan/ViewModels/Dialogs/DeleteColumnConfirmViewModel.cs b/YKanBan/ViewModels/Dialogs/DeleteColumnConfirmViewModel.cs
index ba771b5..097c0bd 100644
--- a/YKanBan/ViewModels/Dialogs/DeleteColumnConfirmViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/DeleteColumnConfirmViewModel.cs
@@ -6,15 +6,13 @@ namespace YKanBan.ViewModels.Dialogs;
/// The confirmation shown before deleting a column, naming the column and how
/// many cards it will cascade-delete.
///
-public sealed class DeleteColumnConfirmViewModel : ViewModelBase
-{
+public sealed class DeleteColumnConfirmViewModel : ViewModelBase {
///
/// Initializes the confirmation.
///
/// The title of the column to delete.
/// The number of cards in the column.
- public DeleteColumnConfirmViewModel(string columnTitle, int cardCount)
- {
+ public DeleteColumnConfirmViewModel(string columnTitle, int cardCount) {
Message = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture,
Resources.Confirm_DeleteColumn_Message,
diff --git a/YKanBan/ViewModels/Dialogs/DeleteTagConfirmViewModel.cs b/YKanBan/ViewModels/Dialogs/DeleteTagConfirmViewModel.cs
index 6047f34..5d80dbc 100644
--- a/YKanBan/ViewModels/Dialogs/DeleteTagConfirmViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/DeleteTagConfirmViewModel.cs
@@ -6,15 +6,13 @@ namespace YKanBan.ViewModels.Dialogs;
/// The confirmation shown before deleting a tag, naming the tag and how many
/// cards it will be removed from.
///
-public sealed class DeleteTagConfirmViewModel : ViewModelBase
-{
+public sealed class DeleteTagConfirmViewModel : ViewModelBase {
///
/// Initializes the confirmation.
///
/// The name of the tag to delete.
/// The number of cards using the tag.
- public DeleteTagConfirmViewModel(string tagName, long usageCount)
- {
+ public DeleteTagConfirmViewModel(string tagName, long usageCount) {
Message = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture,
Resources.DeleteTag_Confirm_Message,
diff --git a/YKanBan/ViewModels/Dialogs/DiscardEditConfirmViewModel.cs b/YKanBan/ViewModels/Dialogs/DiscardEditConfirmViewModel.cs
index 247c7e3..2c6b20a 100644
--- a/YKanBan/ViewModels/Dialogs/DiscardEditConfirmViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/DiscardEditConfirmViewModel.cs
@@ -3,8 +3,7 @@ namespace YKanBan.ViewModels.Dialogs;
///
/// The confirmation shown before closing an editor with unsaved changes.
///
-public sealed class DiscardEditConfirmViewModel : ViewModelBase
-{
+public sealed class DiscardEditConfirmViewModel : ViewModelBase {
///
/// Gets the localized confirmation message.
///
diff --git a/YKanBan/ViewModels/Dialogs/ExportDoneViewModel.cs b/YKanBan/ViewModels/Dialogs/ExportDoneViewModel.cs
index 53ef6f7..a0c73fd 100644
--- a/YKanBan/ViewModels/Dialogs/ExportDoneViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/ExportDoneViewModel.cs
@@ -6,14 +6,12 @@ namespace YKanBan.ViewModels.Dialogs;
/// The confirmation dialog shown after a successful Markdown export, naming the
/// written file.
///
-public sealed class ExportDoneViewModel : ViewModelBase
-{
+public sealed class ExportDoneViewModel : ViewModelBase {
///
/// Initializes the dialog with the exported file path.
///
/// The path the export was written to.
- public ExportDoneViewModel(string filePath)
- {
+ public ExportDoneViewModel(string filePath) {
Message = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture, Resources.Export_Done_Message, filePath);
}
diff --git a/YKanBan/ViewModels/Dialogs/MoveCardViewModel.cs b/YKanBan/ViewModels/Dialogs/MoveCardViewModel.cs
index 1cfd6a4..6a51c78 100644
--- a/YKanBan/ViewModels/Dialogs/MoveCardViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/MoveCardViewModel.cs
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
+
using YKanBan.Models;
namespace YKanBan.ViewModels.Dialogs;
@@ -6,15 +7,13 @@ namespace YKanBan.ViewModels.Dialogs;
///
/// The move-card dialog: choose one of the other columns as the destination.
///
-public sealed partial class MoveCardViewModel : ViewModelBase
-{
+public sealed partial class MoveCardViewModel : ViewModelBase {
///
/// Initializes the dialog with the candidate destination columns.
///
/// All columns; the card's current column is excluded.
/// The card's current column id.
- public MoveCardViewModel(IReadOnlyList columns, long currentColumnId)
- {
+ public MoveCardViewModel(IReadOnlyList columns, long currentColumnId) {
Targets = columns
.Where(column => column.Id != currentColumnId)
.Select(column => new OptionItem(column.Id, column.Title))
diff --git a/YKanBan/ViewModels/Dialogs/SearchErrorViewModel.cs b/YKanBan/ViewModels/Dialogs/SearchErrorViewModel.cs
index 113bcd9..0d04dcc 100644
--- a/YKanBan/ViewModels/Dialogs/SearchErrorViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/SearchErrorViewModel.cs
@@ -6,14 +6,12 @@ namespace YKanBan.ViewModels.Dialogs;
/// The pure-information dialog shown when the search expression is invalid. It
/// wraps the parser's non-localized English detail in the localized message.
///
-public sealed class SearchErrorViewModel : ViewModelBase
-{
+public sealed class SearchErrorViewModel : ViewModelBase {
///
/// Initializes the dialog with the parser's English detail.
///
/// The non-localized parser error description.
- public SearchErrorViewModel(string detail)
- {
+ public SearchErrorViewModel(string detail) {
Message = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture,
Resources.Search_Error_Message,
diff --git a/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs b/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs
index cc83055..de2755f 100644
--- a/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/SettingsDialogViewModel.cs
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
+
using YKanBan.Storage.AppData;
namespace YKanBan.ViewModels.Dialogs;
@@ -8,8 +9,7 @@ namespace YKanBan.ViewModels.Dialogs;
/// confirmation-dialog toggles. Nothing is applied until the user accepts;
/// cancelling or closing the window discards the changes.
///
-public sealed partial class SettingsDialogViewModel : ViewModelBase
-{
+public sealed partial class SettingsDialogViewModel : ViewModelBase {
private readonly AppConfig _config;
private readonly string _startupLanguage;
@@ -18,8 +18,7 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase
///
/// The live application configuration.
/// The language applied at startup, for the restart notice.
- public SettingsDialogViewModel(AppConfig config, string startupLanguage)
- {
+ public SettingsDialogViewModel(AppConfig config, string startupLanguage) {
_config = config;
_startupLanguage = startupLanguage;
@@ -101,14 +100,11 @@ public sealed partial class SettingsDialogViewModel : ViewModelBase
///
/// Commits the working values into the live configuration.
///
- public void Accept()
- {
- if (SelectedLanguage is not null)
- {
+ public void Accept() {
+ if (SelectedLanguage is not null) {
_config.Language = (string)SelectedLanguage.Value;
}
- if (SelectedTheme is not null)
- {
+ if (SelectedTheme is not null) {
_config.Theme = (ThemeOption)SelectedTheme.Value;
App.ApplyTheme(_config.Theme);
}
diff --git a/YKanBan/ViewModels/Dialogs/TagEditorViewModel.cs b/YKanBan/ViewModels/Dialogs/TagEditorViewModel.cs
index 9538d82..95f7f90 100644
--- a/YKanBan/ViewModels/Dialogs/TagEditorViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/TagEditorViewModel.cs
@@ -1,6 +1,9 @@
using Avalonia.Media;
+
using CommunityToolkit.Mvvm.ComponentModel;
+
using Microsoft.Data.Sqlite;
+
using YKanBan.Models;
using YKanBan.Storage.Workspace;
@@ -11,8 +14,7 @@ namespace YKanBan.ViewModels.Dialogs;
/// optional description. The write happens when the dialog is accepted; a
/// duplicate or empty name keeps the dialog open with an inline error.
///
-public sealed partial class TagEditorViewModel : ViewModelBase
-{
+public sealed partial class TagEditorViewModel : ViewModelBase {
private readonly WorkspaceRepository _repository;
private readonly TagModel? _existing;
@@ -21,8 +23,7 @@ public sealed partial class TagEditorViewModel : ViewModelBase
///
/// The workspace repository.
/// The tag to edit, or to add one.
- public TagEditorViewModel(WorkspaceRepository repository, TagModel? existing)
- {
+ public TagEditorViewModel(WorkspaceRepository repository, TagModel? existing) {
_repository = repository;
_existing = existing;
name = existing?.Name ?? string.Empty;
@@ -70,26 +71,19 @@ public sealed partial class TagEditorViewModel : ViewModelBase
/// Writes the tag when accepted.
///
/// when the dialog may close.
- public bool TryAccept()
- {
+ public bool TryAccept() {
ErrorMessage = null;
var rgb = new RgbColor(Color.R, Color.G, Color.B);
- try
- {
- if (_existing is null)
- {
+ try {
+ if (_existing is null) {
_repository.AddTag(Name, rgb, Description);
- }
- else
- {
+ } else {
_repository.UpdateTag(_existing.Id, Name, rgb, Description);
}
return true;
- }
- catch (SqliteException)
- {
+ } catch (SqliteException) {
ErrorMessage = Resources.TagEditor_InvalidName;
return false;
}
diff --git a/YKanBan/ViewModels/Dialogs/TagPickerViewModel.cs b/YKanBan/ViewModels/Dialogs/TagPickerViewModel.cs
index d288db7..d76bab3 100644
--- a/YKanBan/ViewModels/Dialogs/TagPickerViewModel.cs
+++ b/YKanBan/ViewModels/Dialogs/TagPickerViewModel.cs
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel;
+
using YKanBan.Storage.Workspace;
using YKanBan.ViewModels.Board;
@@ -9,15 +10,13 @@ namespace YKanBan.ViewModels.Dialogs;
/// not already assigned to the card. When no candidate remains the picker
/// shows a message and cannot be confirmed.
///
-public sealed partial class TagPickerViewModel : ViewModelBase
-{
+public sealed partial class TagPickerViewModel : ViewModelBase {
///
/// Initializes the picker with the tags not yet assigned to the card.
///
/// The workspace repository.
/// The tag ids already assigned to the card.
- public TagPickerViewModel(WorkspaceRepository repository, IReadOnlyCollection assignedTagIds)
- {
+ public TagPickerViewModel(WorkspaceRepository repository, IReadOnlyCollection assignedTagIds) {
Targets = repository.GetTags()
.Where(tag => !assignedTagIds.Contains(tag.Id))
.Select(tag => new TagBadgeViewModel(tag))
diff --git a/YKanBan/ViewModels/ErrorPages/ArgumentErrorPageViewModel.cs b/YKanBan/ViewModels/ErrorPages/ArgumentErrorPageViewModel.cs
index 4007c14..fc6809c 100644
--- a/YKanBan/ViewModels/ErrorPages/ArgumentErrorPageViewModel.cs
+++ b/YKanBan/ViewModels/ErrorPages/ArgumentErrorPageViewModel.cs
@@ -4,6 +4,5 @@ namespace YKanBan.ViewModels.ErrorPages;
/// The full-window page shown when YKanBan is started without arguments or with
/// invalid arguments. It has no operations.
///
-public sealed class ArgumentErrorPageViewModel : ViewModelBase
-{
+public sealed class ArgumentErrorPageViewModel : ViewModelBase {
}
diff --git a/YKanBan/ViewModels/ErrorPages/DirectoryMissingPageViewModel.cs b/YKanBan/ViewModels/ErrorPages/DirectoryMissingPageViewModel.cs
index 5eb1fd2..a07a196 100644
--- a/YKanBan/ViewModels/ErrorPages/DirectoryMissingPageViewModel.cs
+++ b/YKanBan/ViewModels/ErrorPages/DirectoryMissingPageViewModel.cs
@@ -6,14 +6,12 @@ namespace YKanBan.ViewModels.ErrorPages;
/// The full-window page shown when the folder passed on the command line does
/// not exist. It has no operations.
///
-public sealed class DirectoryMissingPageViewModel : ViewModelBase
-{
+public sealed class DirectoryMissingPageViewModel : ViewModelBase {
///
/// Initializes the page for a missing folder.
///
/// The folder path that does not exist.
- public DirectoryMissingPageViewModel(string folderPath)
- {
+ public DirectoryMissingPageViewModel(string folderPath) {
FolderPath = folderPath;
Message = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture, Resources.Error_DirectoryMissing_Message, folderPath);
diff --git a/YKanBan/ViewModels/ErrorPages/LockConflictPageViewModel.cs b/YKanBan/ViewModels/ErrorPages/LockConflictPageViewModel.cs
index 817cd69..fc20f93 100644
--- a/YKanBan/ViewModels/ErrorPages/LockConflictPageViewModel.cs
+++ b/YKanBan/ViewModels/ErrorPages/LockConflictPageViewModel.cs
@@ -6,14 +6,12 @@ namespace YKanBan.ViewModels.ErrorPages;
/// The full-window page shown when the workspace is already locked by another
/// instance. It has no operations.
///
-public sealed class LockConflictPageViewModel : ViewModelBase
-{
+public sealed class LockConflictPageViewModel : ViewModelBase {
///
/// Initializes the page for a locked workspace.
///
/// The locked workspace folder.
- public LockConflictPageViewModel(string folderPath)
- {
+ public LockConflictPageViewModel(string folderPath) {
FolderPath = folderPath;
Message = string.Format(
Resources.Culture ?? CultureInfo.CurrentCulture, Resources.Error_LockConflict_Message, folderPath);
diff --git a/YKanBan/ViewModels/ErrorPages/NotInitializedPageViewModel.cs b/YKanBan/ViewModels/ErrorPages/NotInitializedPageViewModel.cs
index 40830b9..06fe24a 100644
--- a/YKanBan/ViewModels/ErrorPages/NotInitializedPageViewModel.cs
+++ b/YKanBan/ViewModels/ErrorPages/NotInitializedPageViewModel.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+
using CommunityToolkit.Mvvm.Input;
namespace YKanBan.ViewModels.ErrorPages;
@@ -8,8 +9,7 @@ namespace YKanBan.ViewModels.ErrorPages;
/// structure. It is the only error page with an operation: initializing the
/// workspace.
///
-public sealed partial class NotInitializedPageViewModel : ViewModelBase
-{
+public sealed partial class NotInitializedPageViewModel : ViewModelBase {
private readonly Interaction _initializeWorkspace;
///
@@ -17,8 +17,7 @@ public sealed partial class NotInitializedPageViewModel : ViewModelBase
///
/// The uninitialized folder path.
/// The interaction that performs initialization.
- public NotInitializedPageViewModel(string folderPath, Interaction initializeWorkspace)
- {
+ public NotInitializedPageViewModel(string folderPath, Interaction initializeWorkspace) {
FolderPath = folderPath;
_initializeWorkspace = initializeWorkspace;
Message = string.Format(
diff --git a/YKanBan/ViewModels/Interaction.cs b/YKanBan/ViewModels/Interaction.cs
index 001fe54..9f3d245 100644
--- a/YKanBan/ViewModels/Interaction.cs
+++ b/YKanBan/ViewModels/Interaction.cs
@@ -3,8 +3,7 @@ namespace YKanBan.ViewModels;
///
/// Input placeholder for interactions that carry no meaningful payload.
///
-public readonly record struct Unit
-{
+public readonly record struct Unit {
///
/// Gets the single unit value.
///
@@ -18,8 +17,7 @@ public readonly record struct Unit
/// The view model never holds a reference to a view.
///
/// The request payload.
-public sealed class Interaction
-{
+public sealed class Interaction {
private Func? _handler;
///
@@ -46,8 +44,7 @@ public sealed class Interaction
///
/// The request payload.
/// The handler's answer.
-public sealed class Interaction
-{
+public sealed class Interaction {
private Func>? _handler;
///
diff --git a/YKanBan/ViewModels/MainWindowViewModel.cs b/YKanBan/ViewModels/MainWindowViewModel.cs
index 03f00d3..ab4db4d 100644
--- a/YKanBan/ViewModels/MainWindowViewModel.cs
+++ b/YKanBan/ViewModels/MainWindowViewModel.cs
@@ -1,5 +1,7 @@
using System.Diagnostics;
+
using CommunityToolkit.Mvvm.ComponentModel;
+
using YKanBan.Launching;
using YKanBan.Services;
using YKanBan.Storage;
@@ -13,8 +15,7 @@ namespace YKanBan.ViewModels;
/// normal workspace view or one of the full-window error pages, and owns the
/// interactions that the view fulfils by opening dialogs.
///
-public sealed partial class MainWindowViewModel : ViewModelBase
-{
+public sealed partial class MainWindowViewModel : ViewModelBase {
private readonly AppServices _services;
///
@@ -22,8 +23,7 @@ public sealed partial class MainWindowViewModel : ViewModelBase
///
/// The process composition root.
/// The command-line arguments (excluding the executable).
- public MainWindowViewModel(AppServices services, IReadOnlyList arguments)
- {
+ public MainWindowViewModel(AppServices services, IReadOnlyList arguments) {
_services = services;
ShowSettings = new Interaction();
ShowAbout = new Interaction();
@@ -63,30 +63,22 @@ public sealed partial class MainWindowViewModel : ViewModelBase
/// Resolves the launch arguments into a content view model.
///
/// The command-line arguments.
- private void Resolve(IReadOnlyList arguments)
- {
- if (!LaunchArguments.TryParse(arguments, out string? folderPath))
- {
+ private void Resolve(IReadOnlyList arguments) {
+ if (!LaunchArguments.TryParse(arguments, out string? folderPath)) {
Content = new ArgumentErrorPageViewModel();
return;
}
- if (!Directory.Exists(folderPath))
- {
+ if (!Directory.Exists(folderPath)) {
Content = new DirectoryMissingPageViewModel(folderPath);
return;
}
- try
- {
+ try {
OpenWorkspace(folderPath);
- }
- catch (WorkspaceNotInitializedException)
- {
+ } catch (WorkspaceNotInitializedException) {
Content = new NotInitializedPageViewModel(folderPath, InitializeWorkspace);
- }
- catch (WorkspaceLockException)
- {
+ } catch (WorkspaceLockException) {
Content = new LockConflictPageViewModel(folderPath);
}
}
@@ -95,8 +87,7 @@ public sealed partial class MainWindowViewModel : ViewModelBase
/// Opens the workspace and switches the content to the normal view.
///
/// The workspace folder to open.
- private void OpenWorkspace(string folderPath)
- {
+ private void OpenWorkspace(string folderPath) {
WorkspaceSession session = _services.OpenWorkspace(folderPath);
Content = new WorkspaceViewModel(_services, session, ShowSettings, ShowAbout);
Title = $"{new DirectoryInfo(folderPath).Name} - {Resources.App_Name}";
@@ -107,22 +98,17 @@ public sealed partial class MainWindowViewModel : ViewModelBase
/// columns and opens the resulting workspace.
///
/// A completed task once initialization has been attempted.
- private Task InitializeCurrentWorkspaceAsync()
- {
- if (Content is not NotInitializedPageViewModel page)
- {
+ private Task InitializeCurrentWorkspaceAsync() {
+ if (Content is not NotInitializedPageViewModel page) {
return Task.CompletedTask;
}
- try
- {
+ try {
WorkspaceInitializer.Initialize(page.FolderPath);
WorkspacePreset.AddPresetColumns(page.FolderPath);
WorkspacePreset.AddPresetTags(page.FolderPath);
OpenWorkspace(page.FolderPath);
- }
- catch (Exception exception) when (exception is YKanBanException or IOException or UnauthorizedAccessException)
- {
+ } catch (Exception exception) when (exception is YKanBanException or IOException or UnauthorizedAccessException) {
// Initialization failed; the page remains so the user can retry.
Debug.WriteLine(exception);
}
diff --git a/YKanBan/ViewModels/Tags/TagItemViewModel.cs b/YKanBan/ViewModels/Tags/TagItemViewModel.cs
index 8df4930..8d3f646 100644
--- a/YKanBan/ViewModels/Tags/TagItemViewModel.cs
+++ b/YKanBan/ViewModels/Tags/TagItemViewModel.cs
@@ -1,5 +1,7 @@
using System.Globalization;
+
using CommunityToolkit.Mvvm.Input;
+
using YKanBan.Models;
using YKanBan.ViewModels.Board;
@@ -9,8 +11,7 @@ namespace YKanBan.ViewModels.Tags;
/// One row of the tag manager: a color swatch, the name, the description and the
/// usage count, plus edit and delete actions.
///
-public sealed partial class TagItemViewModel
-{
+public sealed partial class TagItemViewModel {
private readonly TagsViewModel _owner;
///
@@ -19,8 +20,7 @@ public sealed partial class TagItemViewModel
/// The owning tag manager.
/// The tag model.
/// The number of cards using the tag.
- public TagItemViewModel(TagsViewModel owner, TagModel tag, long usageCount)
- {
+ public TagItemViewModel(TagsViewModel owner, TagModel tag, long usageCount) {
_owner = owner;
Tag = tag;
Badge = new TagBadgeViewModel(tag);
diff --git a/YKanBan/ViewModels/Tags/TagsViewModel.cs b/YKanBan/ViewModels/Tags/TagsViewModel.cs
index cff3070..ce70928 100644
--- a/YKanBan/ViewModels/Tags/TagsViewModel.cs
+++ b/YKanBan/ViewModels/Tags/TagsViewModel.cs
@@ -1,5 +1,7 @@
using System.Collections.ObjectModel;
+
using CommunityToolkit.Mvvm.Input;
+
using YKanBan.Models;
using YKanBan.Storage.Workspace;
@@ -10,8 +12,7 @@ namespace YKanBan.ViewModels.Tags;
/// re-describe and delete), showing each tag's color, name, description and
/// usage count. Tag changes also refresh the board.
///
-public sealed partial class TagsViewModel : ViewModelBase
-{
+public sealed partial class TagsViewModel : ViewModelBase {
private readonly WorkspaceRepository _repository;
private readonly Action _boardChanged;
@@ -20,8 +21,7 @@ public sealed partial class TagsViewModel : ViewModelBase
///
/// The workspace repository.
/// Invoked after a tag change so the board refreshes.
- public TagsViewModel(WorkspaceRepository repository, Action boardChanged)
- {
+ public TagsViewModel(WorkspaceRepository repository, Action boardChanged) {
_repository = repository;
_boardChanged = boardChanged;
Reload();
@@ -47,13 +47,19 @@ public sealed partial class TagsViewModel : ViewModelBase
///
public bool HasNoTags => Tags.Count == 0;
- /// Gets the interaction that opens the new-tag editor.
+ ///
+ /// Gets the interaction that opens the new-tag editor.
+ ///
public Interaction AddTag { get; } = new();
- /// Gets the interaction that opens the tag editor.
+ ///
+ /// Gets the interaction that opens the tag editor.
+ ///
public Interaction EditTag { get; } = new();
- /// Gets the interaction that confirms and deletes a tag.
+ ///
+ /// Gets the interaction that confirms and deletes a tag.
+ ///
public Interaction DeleteTag { get; } = new();
///
@@ -79,11 +85,9 @@ public sealed partial class TagsViewModel : ViewModelBase
///
/// Reloads the tag list from the repository.
///
- public void Reload()
- {
+ public void Reload() {
Tags.Clear();
- foreach ((TagModel tag, long usage) in _repository.GetTagsWithUsage())
- {
+ foreach ((TagModel tag, long usage) in _repository.GetTagsWithUsage()) {
Tags.Add(new TagItemViewModel(this, tag, usage));
}
@@ -94,8 +98,7 @@ public sealed partial class TagsViewModel : ViewModelBase
///
/// Reloads the tags and refreshes the board after a tag change.
///
- public void RefreshAfterChange()
- {
+ public void RefreshAfterChange() {
Reload();
_boardChanged();
}
diff --git a/YKanBan/ViewModels/ViewModelBase.cs b/YKanBan/ViewModels/ViewModelBase.cs
index 5b2f8b5..bdd40ea 100644
--- a/YKanBan/ViewModels/ViewModelBase.cs
+++ b/YKanBan/ViewModels/ViewModelBase.cs
@@ -2,6 +2,5 @@
namespace YKanBan.ViewModels;
-public class ViewModelBase : ObservableObject
-{
+public class ViewModelBase : ObservableObject {
}
diff --git a/YKanBan/ViewModels/WorkspaceViewModel.cs b/YKanBan/ViewModels/WorkspaceViewModel.cs
index 2fcadab..38b573f 100644
--- a/YKanBan/ViewModels/WorkspaceViewModel.cs
+++ b/YKanBan/ViewModels/WorkspaceViewModel.cs
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.Input;
+
using YKanBan.Services;
using YKanBan.Storage.Workspace;
using YKanBan.ViewModels.Board;
@@ -10,8 +11,7 @@ namespace YKanBan.ViewModels;
/// The normal workspace view: the top display area (name, path and the export,
/// settings and about buttons) plus the board and tags tabs.
///
-public sealed partial class WorkspaceViewModel : ViewModelBase
-{
+public sealed partial class WorkspaceViewModel : ViewModelBase {
private readonly Interaction _showSettings;
private readonly Interaction _showAbout;
@@ -26,8 +26,7 @@ public sealed partial class WorkspaceViewModel : ViewModelBase
AppServices services,
WorkspaceSession session,
Interaction showSettings,
- Interaction showAbout)
- {
+ Interaction showAbout) {
Session = session;
WorkspaceName = new DirectoryInfo(session.FolderPath).Name;
WorkspacePath = session.FolderPath;
diff --git a/YKanBan/Views/Board/BoardView.axaml.cs b/YKanBan/Views/Board/BoardView.axaml.cs
index 61ca0b3..e508448 100644
--- a/YKanBan/Views/Board/BoardView.axaml.cs
+++ b/YKanBan/Views/Board/BoardView.axaml.cs
@@ -1,24 +1,21 @@
using Avalonia.Controls;
+
using YKanBan.ViewModels.Board;
using YKanBan.ViewModels.Dialogs;
using YKanBan.Views.Dialogs;
namespace YKanBan.Views.Board;
-public partial class BoardView : UserControl
-{
+public partial class BoardView : UserControl {
private bool _registered;
- public BoardView()
- {
+ public BoardView() {
InitializeComponent();
}
- protected override void OnDataContextChanged(EventArgs e)
- {
+ protected override void OnDataContextChanged(EventArgs e) {
base.OnDataContextChanged(e);
- if (DataContext is BoardViewModel board && !_registered)
- {
+ if (DataContext is BoardViewModel board && !_registered) {
_registered = true;
board.AddColumn.RegisterHandler(_ => EditColumnAsync(board, null));
board.EditColumn.RegisterHandler(column => EditColumnAsync(board, column));
@@ -31,47 +28,44 @@ public partial class BoardView : UserControl
}
}
- /// Gets the window that owns this board, used as the dialog owner.
+ ///
+ /// Gets the window that owns this board, used as the dialog owner.
+ ///
private Window? OwningWindow => TopLevel.GetTopLevel(this) as Window;
- /// Shows the add/edit column dialog and reloads on success.
- private async Task EditColumnAsync(BoardViewModel board, ColumnViewModel? column)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Shows the add/edit column dialog and reloads on success.
+ ///
+ private async Task EditColumnAsync(BoardViewModel board, ColumnViewModel? column) {
+ if (OwningWindow is not Window owner) {
return;
}
- var dialog = new ColumnEditorDialog
- {
+ var dialog = new ColumnEditorDialog {
DataContext = new ColumnEditorViewModel(board.Repository, column?.Model),
};
- if (await dialog.ShowDialog(owner))
- {
+ if (await dialog.ShowDialog(owner)) {
board.Reload();
}
}
- /// Confirms and deletes a column, then reloads.
- private async Task DeleteColumnAsync(BoardViewModel board, ColumnViewModel column)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Confirms and deletes a column, then reloads.
+ ///
+ private async Task DeleteColumnAsync(BoardViewModel board, ColumnViewModel column) {
+ if (OwningWindow is not Window owner) {
return;
}
bool confirmed = true;
- if (board.Config.Confirmations.DeleteColumn)
- {
- var dialog = new DeleteColumnConfirmDialog
- {
+ if (board.Config.Confirmations.DeleteColumn) {
+ var dialog = new DeleteColumnConfirmDialog {
DataContext = new DeleteColumnConfirmViewModel(column.Title, column.CardCount),
};
confirmed = await dialog.ShowDialog(owner);
}
- if (!confirmed)
- {
+ if (!confirmed) {
return;
}
@@ -79,45 +73,40 @@ public partial class BoardView : UserControl
board.Reload();
}
- /// Shows the add/edit card dialog and reloads on success.
- private async Task EditCardAsync(BoardViewModel board, ColumnViewModel? column, CardViewModel? card)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Shows the add/edit card dialog and reloads on success.
+ ///
+ private async Task EditCardAsync(BoardViewModel board, ColumnViewModel? column, CardViewModel? card) {
+ if (OwningWindow is not Window owner) {
return;
}
long columnId = card?.ColumnId ?? column!.Id;
- var dialog = new CardEditorDialog
- {
+ var dialog = new CardEditorDialog {
DataContext = new CardEditorViewModel(board.Repository, columnId, card?.Model),
};
- if (await dialog.ShowDialog(owner))
- {
+ if (await dialog.ShowDialog(owner)) {
board.Reload();
}
}
- /// Confirms and deletes a card, then reloads.
- private async Task DeleteCardAsync(BoardViewModel board, CardViewModel card)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Confirms and deletes a card, then reloads.
+ ///
+ private async Task DeleteCardAsync(BoardViewModel board, CardViewModel card) {
+ if (OwningWindow is not Window owner) {
return;
}
bool confirmed = true;
- if (board.Config.Confirmations.DeleteCard)
- {
- var dialog = new DeleteCardConfirmDialog
- {
+ if (board.Config.Confirmations.DeleteCard) {
+ var dialog = new DeleteCardConfirmDialog {
DataContext = new DeleteCardConfirmViewModel(card.Id),
};
confirmed = await dialog.ShowDialog(owner);
}
- if (!confirmed)
- {
+ if (!confirmed) {
return;
}
@@ -125,35 +114,32 @@ public partial class BoardView : UserControl
board.Reload();
}
- /// Shows the move-card dialog and reloads when a destination is chosen.
- private async Task MoveCardAsync(BoardViewModel board, CardViewModel card)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Shows the move-card dialog and reloads when a destination is chosen.
+ ///
+ private async Task MoveCardAsync(BoardViewModel board, CardViewModel card) {
+ if (OwningWindow is not Window owner) {
return;
}
- var dialog = new MoveCardDialog
- {
+ var dialog = new MoveCardDialog {
DataContext = new MoveCardViewModel(board.Repository.GetColumns(), card.ColumnId),
};
- if (await dialog.ShowDialog(owner) is long targetColumnId)
- {
+ if (await dialog.ShowDialog(owner) is long targetColumnId) {
board.Repository.MoveCard(card.Id, targetColumnId);
board.Reload();
}
}
- /// Shows the search syntax error dialog.
- private async Task ShowSearchErrorAsync(string detail)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Shows the search syntax error dialog.
+ ///
+ private async Task ShowSearchErrorAsync(string detail) {
+ if (OwningWindow is not Window owner) {
return;
}
- var dialog = new SearchErrorDialog
- {
+ var dialog = new SearchErrorDialog {
DataContext = new SearchErrorViewModel(detail),
};
await dialog.ShowDialog(owner);
diff --git a/YKanBan/Views/Board/CardView.axaml.cs b/YKanBan/Views/Board/CardView.axaml.cs
index 07dcfc0..de4658b 100644
--- a/YKanBan/Views/Board/CardView.axaml.cs
+++ b/YKanBan/Views/Board/CardView.axaml.cs
@@ -1,25 +1,26 @@
using Avalonia.Controls;
using Avalonia.Input;
+
using YKanBan.ViewModels.Board;
namespace YKanBan.Views.Board;
-public partial class CardView : UserControl
-{
- public CardView()
- {
+public partial class CardView : UserControl {
+ public CardView() {
InitializeComponent();
}
- /// Clicking the card body opens the card editor.
- private void OnCardTapped(object? sender, TappedEventArgs e)
- {
- if (DataContext is CardViewModel card)
- {
+ ///
+ /// Clicking the card body opens the card editor.
+ ///
+ private void OnCardTapped(object? sender, TappedEventArgs e) {
+ if (DataContext is CardViewModel card) {
card.EditCommand.Execute(null);
}
}
- /// Stops the corner menu button's tap from bubbling to the card body.
+ ///
+ /// Stops the corner menu button's tap from bubbling to the card body.
+ ///
private void OnMenuButtonTapped(object? sender, TappedEventArgs e) => e.Handled = true;
}
diff --git a/YKanBan/Views/Board/ColumnView.axaml.cs b/YKanBan/Views/Board/ColumnView.axaml.cs
index 110f6a6..c679391 100644
--- a/YKanBan/Views/Board/ColumnView.axaml.cs
+++ b/YKanBan/Views/Board/ColumnView.axaml.cs
@@ -2,10 +2,8 @@ using Avalonia.Controls;
namespace YKanBan.Views.Board;
-public partial class ColumnView : UserControl
-{
- public ColumnView()
- {
+public partial class ColumnView : UserControl {
+ public ColumnView() {
InitializeComponent();
}
}
diff --git a/YKanBan/Views/Dialogs/AboutDialog.axaml.cs b/YKanBan/Views/Dialogs/AboutDialog.axaml.cs
index 7fb96ea..9fd8fb1 100644
--- a/YKanBan/Views/Dialogs/AboutDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/AboutDialog.axaml.cs
@@ -3,13 +3,13 @@ using Avalonia.Interactivity;
namespace YKanBan.Views.Dialogs;
-public partial class AboutDialog : Window
-{
- public AboutDialog()
- {
+public partial class AboutDialog : Window {
+ public AboutDialog() {
InitializeComponent();
}
- /// Closes the dialog.
+ ///
+ /// Closes the dialog.
+ ///
private void OnCloseClick(object? sender, RoutedEventArgs e) => Close();
}
diff --git a/YKanBan/Views/Dialogs/CardEditorDialog.axaml.cs b/YKanBan/Views/Dialogs/CardEditorDialog.axaml.cs
index f317f1c..dd84eb6 100644
--- a/YKanBan/Views/Dialogs/CardEditorDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/CardEditorDialog.axaml.cs
@@ -1,40 +1,36 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
+
using YKanBan.Models;
using YKanBan.ViewModels;
using YKanBan.ViewModels.Dialogs;
namespace YKanBan.Views.Dialogs;
-public partial class CardEditorDialog : Window
-{
+public partial class CardEditorDialog : Window {
private bool _allowClose;
- public CardEditorDialog()
- {
+ public CardEditorDialog() {
InitializeComponent();
}
- protected override void OnDataContextChanged(EventArgs e)
- {
+ protected override void OnDataContextChanged(EventArgs e) {
base.OnDataContextChanged(e);
- if (DataContext is CardEditorViewModel viewModel)
- {
+ if (DataContext is CardEditorViewModel viewModel) {
viewModel.ShowTagPicker.RegisterHandler(ShowTagPickerAsync);
}
}
- /// Shows the nested tag picker, owned by this dialog.
- private async Task ShowTagPickerAsync(Unit input)
- {
- if (DataContext is not CardEditorViewModel viewModel)
- {
+ ///
+ /// Shows the nested tag picker, owned by this dialog.
+ ///
+ private async Task ShowTagPickerAsync(Unit input) {
+ if (DataContext is not CardEditorViewModel viewModel) {
return null;
}
- var dialog = new TagPickerDialog
- {
+ var dialog = new TagPickerDialog {
DataContext = new TagPickerViewModel(
viewModel.Repository,
viewModel.AssignedTags.Select(row => row.Tag.Id).ToList()),
@@ -42,47 +38,45 @@ public partial class CardEditorDialog : Window
return await dialog.ShowDialog(this);
}
- /// Writes the card and closes when accepted.
- private void OnOkClick(object? sender, RoutedEventArgs e)
- {
- if (DataContext is CardEditorViewModel viewModel && viewModel.TryAccept())
- {
+ ///
+ /// Writes the card and closes when accepted.
+ ///
+ private void OnOkClick(object? sender, RoutedEventArgs e) {
+ if (DataContext is CardEditorViewModel viewModel && viewModel.TryAccept()) {
_allowClose = true;
Close(true);
}
}
- /// Requests a close, confirming the discard first when needed.
+ ///
+ /// Requests a close, confirming the discard first when needed.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => _ = RequestCloseAsync();
- /// Intercepts the window close button to run the discard confirmation.
- protected override void OnClosing(WindowClosingEventArgs e)
- {
+ ///
+ /// Intercepts the window close button to run the discard confirmation.
+ ///
+ protected override void OnClosing(WindowClosingEventArgs e) {
base.OnClosing(e);
- if (_allowClose)
- {
+ if (_allowClose) {
return;
}
- if (HasUnsavedChanges && DiscardConfirmEnabled)
- {
+ if (HasUnsavedChanges && DiscardConfirmEnabled) {
e.Cancel = true;
Dispatcher.UIThread.Post(() => _ = RequestCloseAsync());
- }
- else
- {
+ } else {
_allowClose = true;
}
}
- /// Confirms the discard and closes, or stays open when declined.
- private async Task RequestCloseAsync()
- {
- if (HasUnsavedChanges && DiscardConfirmEnabled)
- {
+ ///
+ /// Confirms the discard and closes, or stays open when declined.
+ ///
+ private async Task RequestCloseAsync() {
+ if (HasUnsavedChanges && DiscardConfirmEnabled) {
var dialog = new DiscardEditConfirmDialog { DataContext = new DiscardEditConfirmViewModel() };
- if (!await dialog.ShowDialog(this))
- {
+ if (!await dialog.ShowDialog(this)) {
return;
}
}
@@ -91,9 +85,13 @@ public partial class CardEditorDialog : Window
Close(false);
}
- /// Gets whether the editor has unsaved changes.
+ ///
+ /// Gets whether the editor has unsaved changes.
+ ///
private bool HasUnsavedChanges => DataContext is CardEditorViewModel viewModel && viewModel.HasChanges;
- /// Gets whether the discard confirmation setting is enabled.
+ ///
+ /// Gets whether the discard confirmation setting is enabled.
+ ///
private static bool DiscardConfirmEnabled => App.Services?.Config.Confirmations.DiscardEdit == true;
}
diff --git a/YKanBan/Views/Dialogs/ColumnEditorDialog.axaml.cs b/YKanBan/Views/Dialogs/ColumnEditorDialog.axaml.cs
index 315bd60..f1d9e4d 100644
--- a/YKanBan/Views/Dialogs/ColumnEditorDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/ColumnEditorDialog.axaml.cs
@@ -1,25 +1,26 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
+
using YKanBan.ViewModels.Dialogs;
namespace YKanBan.Views.Dialogs;
-public partial class ColumnEditorDialog : Window
-{
- public ColumnEditorDialog()
- {
+public partial class ColumnEditorDialog : Window {
+ public ColumnEditorDialog() {
InitializeComponent();
}
- /// Writes the column and closes when accepted.
- private void OnOkClick(object? sender, RoutedEventArgs e)
- {
- if (DataContext is ColumnEditorViewModel viewModel && viewModel.TryAccept())
- {
+ ///
+ /// Writes the column and closes when accepted.
+ ///
+ private void OnOkClick(object? sender, RoutedEventArgs e) {
+ if (DataContext is ColumnEditorViewModel viewModel && viewModel.TryAccept()) {
Close(true);
}
}
- /// Closes without writing.
+ ///
+ /// Closes without writing.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
}
diff --git a/YKanBan/Views/Dialogs/DeleteCardConfirmDialog.axaml.cs b/YKanBan/Views/Dialogs/DeleteCardConfirmDialog.axaml.cs
index a49ea73..b76e52b 100644
--- a/YKanBan/Views/Dialogs/DeleteCardConfirmDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/DeleteCardConfirmDialog.axaml.cs
@@ -3,16 +3,18 @@ using Avalonia.Interactivity;
namespace YKanBan.Views.Dialogs;
-public partial class DeleteCardConfirmDialog : Window
-{
- public DeleteCardConfirmDialog()
- {
+public partial class DeleteCardConfirmDialog : Window {
+ public DeleteCardConfirmDialog() {
InitializeComponent();
}
- /// Confirms the deletion.
+ ///
+ /// Confirms the deletion.
+ ///
private void OnConfirmClick(object? sender, RoutedEventArgs e) => Close(true);
- /// Cancels the deletion.
+ ///
+ /// Cancels the deletion.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
}
diff --git a/YKanBan/Views/Dialogs/DeleteColumnConfirmDialog.axaml.cs b/YKanBan/Views/Dialogs/DeleteColumnConfirmDialog.axaml.cs
index 4fea518..80accf1 100644
--- a/YKanBan/Views/Dialogs/DeleteColumnConfirmDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/DeleteColumnConfirmDialog.axaml.cs
@@ -3,16 +3,18 @@ using Avalonia.Interactivity;
namespace YKanBan.Views.Dialogs;
-public partial class DeleteColumnConfirmDialog : Window
-{
- public DeleteColumnConfirmDialog()
- {
+public partial class DeleteColumnConfirmDialog : Window {
+ public DeleteColumnConfirmDialog() {
InitializeComponent();
}
- /// Confirms the deletion.
+ ///
+ /// Confirms the deletion.
+ ///
private void OnConfirmClick(object? sender, RoutedEventArgs e) => Close(true);
- /// Cancels the deletion.
+ ///
+ /// Cancels the deletion.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
}
diff --git a/YKanBan/Views/Dialogs/DeleteTagConfirmDialog.axaml.cs b/YKanBan/Views/Dialogs/DeleteTagConfirmDialog.axaml.cs
index 5f756e2..4f7ffa1 100644
--- a/YKanBan/Views/Dialogs/DeleteTagConfirmDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/DeleteTagConfirmDialog.axaml.cs
@@ -3,16 +3,18 @@ using Avalonia.Interactivity;
namespace YKanBan.Views.Dialogs;
-public partial class DeleteTagConfirmDialog : Window
-{
- public DeleteTagConfirmDialog()
- {
+public partial class DeleteTagConfirmDialog : Window {
+ public DeleteTagConfirmDialog() {
InitializeComponent();
}
- /// Confirms the deletion.
+ ///
+ /// Confirms the deletion.
+ ///
private void OnConfirmClick(object? sender, RoutedEventArgs e) => Close(true);
- /// Cancels the deletion.
+ ///
+ /// Cancels the deletion.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
}
diff --git a/YKanBan/Views/Dialogs/DiscardEditConfirmDialog.axaml.cs b/YKanBan/Views/Dialogs/DiscardEditConfirmDialog.axaml.cs
index caa6a36..a82f0ac 100644
--- a/YKanBan/Views/Dialogs/DiscardEditConfirmDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/DiscardEditConfirmDialog.axaml.cs
@@ -3,16 +3,18 @@ using Avalonia.Interactivity;
namespace YKanBan.Views.Dialogs;
-public partial class DiscardEditConfirmDialog : Window
-{
- public DiscardEditConfirmDialog()
- {
+public partial class DiscardEditConfirmDialog : Window {
+ public DiscardEditConfirmDialog() {
InitializeComponent();
}
- /// Confirms discarding the changes.
+ ///
+ /// Confirms discarding the changes.
+ ///
private void OnConfirmClick(object? sender, RoutedEventArgs e) => Close(true);
- /// Keeps editing.
+ ///
+ /// Keeps editing.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
}
diff --git a/YKanBan/Views/Dialogs/ExportDoneDialog.axaml.cs b/YKanBan/Views/Dialogs/ExportDoneDialog.axaml.cs
index 87655ec..50ca3d1 100644
--- a/YKanBan/Views/Dialogs/ExportDoneDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/ExportDoneDialog.axaml.cs
@@ -3,13 +3,13 @@ using Avalonia.Interactivity;
namespace YKanBan.Views.Dialogs;
-public partial class ExportDoneDialog : Window
-{
- public ExportDoneDialog()
- {
+public partial class ExportDoneDialog : Window {
+ public ExportDoneDialog() {
InitializeComponent();
}
- /// Closes the dialog.
+ ///
+ /// Closes the dialog.
+ ///
private void OnOkClick(object? sender, RoutedEventArgs e) => Close();
}
diff --git a/YKanBan/Views/Dialogs/MoveCardDialog.axaml.cs b/YKanBan/Views/Dialogs/MoveCardDialog.axaml.cs
index ad68989..490802c 100644
--- a/YKanBan/Views/Dialogs/MoveCardDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/MoveCardDialog.axaml.cs
@@ -1,25 +1,26 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
+
using YKanBan.ViewModels.Dialogs;
namespace YKanBan.Views.Dialogs;
-public partial class MoveCardDialog : Window
-{
- public MoveCardDialog()
- {
+public partial class MoveCardDialog : Window {
+ public MoveCardDialog() {
InitializeComponent();
}
- /// Closes with the selected destination column id.
- private void OnOkClick(object? sender, RoutedEventArgs e)
- {
- if (DataContext is MoveCardViewModel viewModel && viewModel.SelectedTarget is not null)
- {
+ ///
+ /// Closes with the selected destination column id.
+ ///
+ private void OnOkClick(object? sender, RoutedEventArgs e) {
+ if (DataContext is MoveCardViewModel viewModel && viewModel.SelectedTarget is not null) {
Close((long)viewModel.SelectedTarget.Value);
}
}
- /// Closes without moving.
+ ///
+ /// Closes without moving.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(null);
}
diff --git a/YKanBan/Views/Dialogs/SearchErrorDialog.axaml.cs b/YKanBan/Views/Dialogs/SearchErrorDialog.axaml.cs
index 3ccbdb5..c13d5c0 100644
--- a/YKanBan/Views/Dialogs/SearchErrorDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/SearchErrorDialog.axaml.cs
@@ -3,13 +3,13 @@ using Avalonia.Interactivity;
namespace YKanBan.Views.Dialogs;
-public partial class SearchErrorDialog : Window
-{
- public SearchErrorDialog()
- {
+public partial class SearchErrorDialog : Window {
+ public SearchErrorDialog() {
InitializeComponent();
}
- /// Closes the dialog.
+ ///
+ /// Closes the dialog.
+ ///
private void OnOkClick(object? sender, RoutedEventArgs e) => Close();
}
diff --git a/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs b/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs
index ba0cac9..cf42918 100644
--- a/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/SettingsDialog.axaml.cs
@@ -1,23 +1,25 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
+
using YKanBan.ViewModels.Dialogs;
namespace YKanBan.Views.Dialogs;
-public partial class SettingsDialog : Window
-{
- public SettingsDialog()
- {
+public partial class SettingsDialog : Window {
+ public SettingsDialog() {
InitializeComponent();
}
- /// Applies the working settings and closes with a positive result.
- private void OnOkClick(object? sender, RoutedEventArgs e)
- {
+ ///
+ /// Applies the working settings and closes with a positive result.
+ ///
+ private void OnOkClick(object? sender, RoutedEventArgs e) {
(DataContext as SettingsDialogViewModel)?.Accept();
Close(true);
}
- /// Closes without applying anything.
+ ///
+ /// Closes without applying anything.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
}
diff --git a/YKanBan/Views/Dialogs/TagEditorDialog.axaml.cs b/YKanBan/Views/Dialogs/TagEditorDialog.axaml.cs
index 3f9f8da..a459821 100644
--- a/YKanBan/Views/Dialogs/TagEditorDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/TagEditorDialog.axaml.cs
@@ -1,25 +1,26 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
+
using YKanBan.ViewModels.Dialogs;
namespace YKanBan.Views.Dialogs;
-public partial class TagEditorDialog : Window
-{
- public TagEditorDialog()
- {
+public partial class TagEditorDialog : Window {
+ public TagEditorDialog() {
InitializeComponent();
}
- /// Writes the tag and closes when accepted.
- private void OnOkClick(object? sender, RoutedEventArgs e)
- {
- if (DataContext is TagEditorViewModel viewModel && viewModel.TryAccept())
- {
+ ///
+ /// Writes the tag and closes when accepted.
+ ///
+ private void OnOkClick(object? sender, RoutedEventArgs e) {
+ if (DataContext is TagEditorViewModel viewModel && viewModel.TryAccept()) {
Close(true);
}
}
- /// Closes without writing.
+ ///
+ /// Closes without writing.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(false);
}
diff --git a/YKanBan/Views/Dialogs/TagPickerDialog.axaml.cs b/YKanBan/Views/Dialogs/TagPickerDialog.axaml.cs
index c0343dc..42be563 100644
--- a/YKanBan/Views/Dialogs/TagPickerDialog.axaml.cs
+++ b/YKanBan/Views/Dialogs/TagPickerDialog.axaml.cs
@@ -1,25 +1,26 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
+
using YKanBan.ViewModels.Dialogs;
namespace YKanBan.Views.Dialogs;
-public partial class TagPickerDialog : Window
-{
- public TagPickerDialog()
- {
+public partial class TagPickerDialog : Window {
+ public TagPickerDialog() {
InitializeComponent();
}
- /// Closes with the selected tag.
- private void OnOkClick(object? sender, RoutedEventArgs e)
- {
- if (DataContext is TagPickerViewModel viewModel && viewModel.SelectedTarget is { } badge)
- {
+ ///
+ /// Closes with the selected tag.
+ ///
+ private void OnOkClick(object? sender, RoutedEventArgs e) {
+ if (DataContext is TagPickerViewModel viewModel && viewModel.SelectedTarget is { } badge) {
Close(badge.Tag);
}
}
- /// Closes without picking a tag.
+ ///
+ /// Closes without picking a tag.
+ ///
private void OnCancelClick(object? sender, RoutedEventArgs e) => Close(null);
}
diff --git a/YKanBan/Views/ErrorPages/ArgumentErrorPageView.axaml.cs b/YKanBan/Views/ErrorPages/ArgumentErrorPageView.axaml.cs
index 2dc50a8..2fbed49 100644
--- a/YKanBan/Views/ErrorPages/ArgumentErrorPageView.axaml.cs
+++ b/YKanBan/Views/ErrorPages/ArgumentErrorPageView.axaml.cs
@@ -2,10 +2,8 @@ using Avalonia.Controls;
namespace YKanBan.Views.ErrorPages;
-public partial class ArgumentErrorPageView : UserControl
-{
- public ArgumentErrorPageView()
- {
+public partial class ArgumentErrorPageView : UserControl {
+ public ArgumentErrorPageView() {
InitializeComponent();
}
}
diff --git a/YKanBan/Views/ErrorPages/DirectoryMissingPageView.axaml.cs b/YKanBan/Views/ErrorPages/DirectoryMissingPageView.axaml.cs
index aa4c9f0..a18f359 100644
--- a/YKanBan/Views/ErrorPages/DirectoryMissingPageView.axaml.cs
+++ b/YKanBan/Views/ErrorPages/DirectoryMissingPageView.axaml.cs
@@ -2,10 +2,8 @@ using Avalonia.Controls;
namespace YKanBan.Views.ErrorPages;
-public partial class DirectoryMissingPageView : UserControl
-{
- public DirectoryMissingPageView()
- {
+public partial class DirectoryMissingPageView : UserControl {
+ public DirectoryMissingPageView() {
InitializeComponent();
}
}
diff --git a/YKanBan/Views/ErrorPages/LockConflictPageView.axaml.cs b/YKanBan/Views/ErrorPages/LockConflictPageView.axaml.cs
index 0905d8e..11d3de8 100644
--- a/YKanBan/Views/ErrorPages/LockConflictPageView.axaml.cs
+++ b/YKanBan/Views/ErrorPages/LockConflictPageView.axaml.cs
@@ -2,10 +2,8 @@ using Avalonia.Controls;
namespace YKanBan.Views.ErrorPages;
-public partial class LockConflictPageView : UserControl
-{
- public LockConflictPageView()
- {
+public partial class LockConflictPageView : UserControl {
+ public LockConflictPageView() {
InitializeComponent();
}
}
diff --git a/YKanBan/Views/ErrorPages/NotInitializedPageView.axaml.cs b/YKanBan/Views/ErrorPages/NotInitializedPageView.axaml.cs
index 0c90457..acc93af 100644
--- a/YKanBan/Views/ErrorPages/NotInitializedPageView.axaml.cs
+++ b/YKanBan/Views/ErrorPages/NotInitializedPageView.axaml.cs
@@ -2,10 +2,8 @@ using Avalonia.Controls;
namespace YKanBan.Views.ErrorPages;
-public partial class NotInitializedPageView : UserControl
-{
- public NotInitializedPageView()
- {
+public partial class NotInitializedPageView : UserControl {
+ public NotInitializedPageView() {
InitializeComponent();
}
}
diff --git a/YKanBan/Views/MainWindow.axaml.cs b/YKanBan/Views/MainWindow.axaml.cs
index 6cbaf8f..29730ec 100644
--- a/YKanBan/Views/MainWindow.axaml.cs
+++ b/YKanBan/Views/MainWindow.axaml.cs
@@ -1,50 +1,46 @@
using Avalonia.Controls;
+
using YKanBan.ViewModels;
using YKanBan.ViewModels.Dialogs;
using YKanBan.Views.Dialogs;
namespace YKanBan.Views;
-public partial class MainWindow : Window
-{
+public partial class MainWindow : Window {
private bool _interactionsRegistered;
- public MainWindow()
- {
+ public MainWindow() {
InitializeComponent();
}
- protected override void OnDataContextChanged(EventArgs e)
- {
+ protected override void OnDataContextChanged(EventArgs e) {
base.OnDataContextChanged(e);
- if (DataContext is MainWindowViewModel viewModel && !_interactionsRegistered)
- {
+ if (DataContext is MainWindowViewModel viewModel && !_interactionsRegistered) {
_interactionsRegistered = true;
viewModel.ShowSettings.RegisterHandler(ShowSettingsAsync);
viewModel.ShowAbout.RegisterHandler(ShowAboutAsync);
}
}
- /// Shows the settings dialog centered on this window.
- private async Task ShowSettingsAsync(Unit input)
- {
- if (App.Services is null)
- {
+ ///
+ /// Shows the settings dialog centered on this window.
+ ///
+ private async Task ShowSettingsAsync(Unit input) {
+ if (App.Services is null) {
return;
}
- var dialog = new SettingsDialog
- {
+ var dialog = new SettingsDialog {
DataContext = new SettingsDialogViewModel(App.Services.Config, App.StartupLanguage),
};
await dialog.ShowDialog(this);
}
- /// Shows the about dialog centered on this window.
- private async Task ShowAboutAsync(Unit input)
- {
- var dialog = new AboutDialog
- {
+ ///
+ /// Shows the about dialog centered on this window.
+ ///
+ private async Task ShowAboutAsync(Unit input) {
+ var dialog = new AboutDialog {
DataContext = new AboutDialogViewModel(),
};
await dialog.ShowDialog(this);
diff --git a/YKanBan/Views/Tags/TagsView.axaml.cs b/YKanBan/Views/Tags/TagsView.axaml.cs
index ca7cc76..d51d415 100644
--- a/YKanBan/Views/Tags/TagsView.axaml.cs
+++ b/YKanBan/Views/Tags/TagsView.axaml.cs
@@ -1,24 +1,21 @@
using Avalonia.Controls;
+
using YKanBan.ViewModels.Dialogs;
using YKanBan.ViewModels.Tags;
using YKanBan.Views.Dialogs;
namespace YKanBan.Views.Tags;
-public partial class TagsView : UserControl
-{
+public partial class TagsView : UserControl {
private bool _registered;
- public TagsView()
- {
+ public TagsView() {
InitializeComponent();
}
- protected override void OnDataContextChanged(EventArgs e)
- {
+ protected override void OnDataContextChanged(EventArgs e) {
base.OnDataContextChanged(e);
- if (DataContext is TagsViewModel tags && !_registered)
- {
+ if (DataContext is TagsViewModel tags && !_registered) {
_registered = true;
tags.AddTag.RegisterHandler(_ => EditTagAsync(tags, null));
tags.EditTag.RegisterHandler(item => EditTagAsync(tags, item));
@@ -26,47 +23,44 @@ public partial class TagsView : UserControl
}
}
- /// Gets the window that owns this view, used as the dialog owner.
+ ///
+ /// Gets the window that owns this view, used as the dialog owner.
+ ///
private Window? OwningWindow => TopLevel.GetTopLevel(this) as Window;
- /// Shows the add/edit tag dialog and refreshes on success.
- private async Task EditTagAsync(TagsViewModel tags, TagItemViewModel? item)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Shows the add/edit tag dialog and refreshes on success.
+ ///
+ private async Task EditTagAsync(TagsViewModel tags, TagItemViewModel? item) {
+ if (OwningWindow is not Window owner) {
return;
}
- var dialog = new TagEditorDialog
- {
+ var dialog = new TagEditorDialog {
DataContext = new TagEditorViewModel(tags.Repository, item?.Tag),
};
- if (await dialog.ShowDialog(owner))
- {
+ if (await dialog.ShowDialog(owner)) {
tags.RefreshAfterChange();
}
}
- /// Confirms and deletes a tag, then refreshes.
- private async Task DeleteTagAsync(TagsViewModel tags, TagItemViewModel item)
- {
- if (OwningWindow is not Window owner)
- {
+ ///
+ /// Confirms and deletes a tag, then refreshes.
+ ///
+ private async Task DeleteTagAsync(TagsViewModel tags, TagItemViewModel item) {
+ if (OwningWindow is not Window owner) {
return;
}
bool confirmed = true;
- if (App.Services?.Config.Confirmations.DeleteTag == true)
- {
- var dialog = new DeleteTagConfirmDialog
- {
+ if (App.Services?.Config.Confirmations.DeleteTag == true) {
+ var dialog = new DeleteTagConfirmDialog {
DataContext = new DeleteTagConfirmViewModel(item.Name, item.UsageCount),
};
confirmed = await dialog.ShowDialog(owner);
}
- if (!confirmed)
- {
+ if (!confirmed) {
return;
}
diff --git a/YKanBan/Views/WorkspaceView.axaml.cs b/YKanBan/Views/WorkspaceView.axaml.cs
index 17f126c..9b99113 100644
--- a/YKanBan/Views/WorkspaceView.axaml.cs
+++ b/YKanBan/Views/WorkspaceView.axaml.cs
@@ -1,55 +1,51 @@
using System.Text;
+
using Avalonia.Controls;
using Avalonia.Platform.Storage;
+
using YKanBan.Export;
using YKanBan.ViewModels;
using YKanBan.ViewModels.Dialogs;
using YKanBan.Views.Dialogs;
+
using Resx = YKanBan.Resources;
namespace YKanBan.Views;
-public partial class WorkspaceView : UserControl
-{
+public partial class WorkspaceView : UserControl {
private bool _registered;
- public WorkspaceView()
- {
+ public WorkspaceView() {
InitializeComponent();
}
- protected override void OnDataContextChanged(EventArgs e)
- {
+ protected override void OnDataContextChanged(EventArgs e) {
base.OnDataContextChanged(e);
- if (DataContext is WorkspaceViewModel viewModel && !_registered)
- {
+ if (DataContext is WorkspaceViewModel viewModel && !_registered) {
_registered = true;
viewModel.Export.RegisterHandler(ExportAsync);
}
}
- /// Prompts for a file and writes the Markdown board export to it.
- private async Task ExportAsync(Unit input)
- {
- if (DataContext is not WorkspaceViewModel viewModel)
- {
+ ///
+ /// Prompts for a file and writes the Markdown board export to it.
+ ///
+ private async Task ExportAsync(Unit input) {
+ if (DataContext is not WorkspaceViewModel viewModel) {
return;
}
- if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storage)
- {
+ if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storage) {
return;
}
DateTimeOffset exportedAt = DateTimeOffset.Now;
- IStorageFile? file = await storage.SaveFilePickerAsync(new FilePickerSaveOptions
- {
+ IStorageFile? file = await storage.SaveFilePickerAsync(new FilePickerSaveOptions {
Title = Resx.Main_Export,
SuggestedFileName = MarkdownExporter.BuildFileName(viewModel.WorkspaceName, exportedAt),
DefaultExtension = "md",
FileTypeChoices = [new FilePickerFileType("Markdown") { Patterns = ["*.md"] }],
});
- if (file is null)
- {
+ if (file is null) {
return;
}
@@ -62,15 +58,12 @@ public partial class WorkspaceView : UserControl
exportedAt);
await using (Stream stream = await file.OpenWriteAsync())
- await using (var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)))
- {
+ await using (var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))) {
await writer.WriteAsync(markdown);
}
- if (TopLevel.GetTopLevel(this) is Window owner)
- {
- var dialog = new ExportDoneDialog
- {
+ if (TopLevel.GetTopLevel(this) is Window owner) {
+ var dialog = new ExportDoneDialog {
DataContext = new ExportDoneViewModel(file.TryGetLocalPath() ?? file.Name),
};
await dialog.ShowDialog(owner);