Initial commit: WinUI 3 confessional library app with CI/CD

This commit is contained in:
ReformedWitness
2026-05-01 15:51:59 -05:00
commit d5e18060d4
146 changed files with 178928 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="Confessio.Views.HomePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Confessio"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
SizeChanged="Page_SizeChanged">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel x:Name="SummaryPanel" Margin="36,24,36,0" Grid.Row="0" Spacing="4">
<TextBlock x:Name="PageTitleText"
Text="All confessions"
Style="{StaticResource TitleTextBlockStyle}"
AutomationProperties.HeadingLevel="Level1" />
<TextBlock x:Name="CountText"
Style="{StaticResource BodyTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
<GridView x:Name="DocumentGridView"
Grid.Row="1"
Padding="36,16,36,36"
ItemsSource="{x:Bind FilteredDocuments, Mode=OneWay}"
IsItemClickEnabled="True"
IsSwipeEnabled="False"
ItemClick="DocumentGridView_ItemClick"
SelectionMode="None"
AutomationProperties.AutomationId="DocumentGridView"
AutomationProperties.Name="Confession documents">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="MinWidth" Value="312" />
<Setter Property="MaxWidth" Value="360" />
<Setter Property="MinHeight" Value="116" />
<Setter Property="Margin" Value="0,0,12,12" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemTemplate>
<DataTemplate x:DataType="local:ConfessionDocument">
<Border Style="{StaticResource LibraryCardBorderStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Width="44"
Height="44"
CornerRadius="8"
Background="{ThemeResource AccentFillColorSecondaryBrush}"
VerticalAlignment="Top"
Margin="0,0,16,0">
<FontIcon Glyph="{x:Bind CollectionIcon}"
FontSize="22"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1" Spacing="8">
<TextBlock Text="{x:Bind DisplayTitle}"
Style="{StaticResource BodyStrongTextBlockStyle}"
TextWrapping="WrapWholeWords"
MaxLines="2" />
<TextBlock Text="{x:Bind LibraryDetail}"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextTrimming="CharacterEllipsis" />
<TextBlock Text="{x:Bind EntrySummary}"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
<StackPanel x:Name="EmptyStatePanel"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed">
<FontIcon Glyph="&#xE721;" FontSize="48" Foreground="{ThemeResource TextFillColorSecondaryBrush}" HorizontalAlignment="Center" />
<TextBlock Text="No confessions found" Style="{StaticResource SubtitleTextBlockStyle}" HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
</Grid>
</Page>
+60
View File
@@ -0,0 +1,60 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using System.Collections.ObjectModel;
namespace Confessio.Views;
public sealed partial class HomePage : Page
{
public ObservableCollection<ConfessionDocument> FilteredDocuments { get; } = [];
public HomePage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is string collection)
{
AppState.CurrentCollection = collection == "All" ? "All" : collection;
}
RefreshView();
}
public void RefreshView()
{
string query = (App.Current as App)?.MainWindow?.GetSearchQuery() ?? string.Empty;
AppState.ApplyFilters(query);
FilteredDocuments.Clear();
foreach (ConfessionDocument doc in AppState.FilteredDocuments)
{
FilteredDocuments.Add(doc);
}
PageTitleText.Text = AppState.CurrentCollection == "All" ? "All confessions" : AppState.CurrentCollection;
CountText.Text = $"{FilteredDocuments.Count} documents";
EmptyStatePanel.Visibility = FilteredDocuments.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
private void DocumentGridView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is ConfessionDocument document)
{
(App.Current as App)?.MainWindow?.NavigateToReader(document);
}
}
private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
{
bool narrow = e.NewSize.Width < 640;
SummaryPanel.Margin = narrow ? new Thickness(16, 24, 16, 0) : new Thickness(36, 24, 36, 0);
DocumentGridView.Padding = narrow ? new Thickness(16, 16, 16, 24) : new Thickness(36, 16, 36, 36);
}
}
+84
View File
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="Confessio.Views.ReaderPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Confessio"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
SizeChanged="Page_SizeChanged">
<Grid x:Name="RootPanel" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="OutlineColumn" Width="300" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border x:Name="OutlinePane"
Grid.Row="1"
Grid.Column="0"
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,1,0">
<ScrollViewer VerticalScrollBarVisibility="Auto" VerticalScrollMode="Auto">
<StackPanel x:Name="OutlineStack" Margin="8,16,8,16" Spacing="4" />
</ScrollViewer>
</Border>
<ScrollViewer x:Name="ReaderScrollViewer"
Grid.Row="1"
Grid.Column="1"
Padding="48,24,48,48"
VerticalScrollBarVisibility="Auto"
VerticalScrollMode="Auto">
<StackPanel x:Name="ReaderStack" MaxWidth="800" HorizontalAlignment="Center" Spacing="32">
<StackPanel x:Name="ContentHeader" Spacing="16" HorizontalAlignment="Center">
<TextBlock x:Name="DocumentTitleText"
FontSize="32"
FontWeight="SemiBold"
FontFamily="Georgia"
TextWrapping="WrapWholeWords"
TextAlignment="Center"
AutomationProperties.HeadingLevel="Level1" />
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center">
<TextBlock x:Name="MetadataYear"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="•"
Foreground="{ThemeResource TextFillColorTertiaryBrush}" />
<TextBlock x:Name="MetadataFormat"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="•"
Foreground="{ThemeResource TextFillColorTertiaryBrush}" />
<TextBlock x:Name="MetadataAuthors"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
<TextBlock x:Name="MetadataLocation"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
TextAlignment="Center" />
</StackPanel>
<StackPanel x:Name="EntryStack" Spacing="16" />
</StackPanel>
</ScrollViewer>
<StackPanel x:Name="EmptyStatePanel"
Grid.Row="1"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed">
<FontIcon Glyph="&#xE721;" FontSize="48" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock Text="Select a confession to read" HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
</Grid>
</Page>
+278
View File
@@ -0,0 +1,278 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.UI.Text;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Confessio.Views;
public sealed partial class ReaderPage : Page
{
private ConfessionDocument? _document;
public ReaderPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is ConfessionDocument document) LoadDocument(document);
}
private void LoadDocument(ConfessionDocument? document)
{
_document = document;
OutlineStack.Children.Clear();
EntryStack.Children.Clear();
if (document == null)
{
EmptyStatePanel.Visibility = Visibility.Visible;
return;
}
EmptyStatePanel.Visibility = Visibility.Collapsed;
BuildOutline(document);
DocumentTitleText.Text = document.DisplayTitle;
MetadataYear.Text = document.Year;
MetadataFormat.Text = document.DisplayFormat;
MetadataAuthors.Text = document.Authors;
MetadataLocation.Text = document.Location;
string? lastGroupTitle = null;
foreach (ConfessionEntry entry in document.Entries)
{
if (!string.IsNullOrWhiteSpace(entry.GroupTitle) && entry.GroupTitle != lastGroupTitle)
{
lastGroupTitle = entry.GroupTitle;
EntryStack.Children.Add(CreateChapterHeading(entry.GroupTitle));
}
EntryStack.Children.Add(CreateEntryElement(entry));
}
}
private TextBlock CreateChapterHeading(string title)
{
return new TextBlock
{
Text = title,
FontSize = 24,
FontWeight = FontWeights.Bold,
FontFamily = new FontFamily("Georgia"),
Margin = new Thickness(0, 16, 0, 8),
Foreground = (Brush)Application.Current.Resources["TextFillColorPrimaryBrush"]
};
}
public void ReRender() => LoadDocument(_document);
public void RefreshOutlineFromSearch(string query) => LoadDocument(_document);
private void BuildOutline(ConfessionDocument document)
{
bool isCatechism = document.EntryLabel == "Questions";
if (isCatechism)
{
foreach (ConfessionEntry entry in document.Entries)
{
string title = entry.NavigationTitle;
OutlineStack.Children.Add(CreateOutlineButton(title, entry, isHeader: false, isNested: false));
}
return;
}
var chapters = document.Entries
.Where(e => !string.IsNullOrWhiteSpace(e.GroupTitle))
.Select(e => e.GroupTitle!)
.Distinct()
.ToList();
if (chapters.Count > 0)
{
foreach (string chapter in chapters)
{
ConfessionEntry? firstEntryInChapter = document.Entries.FirstOrDefault(e => e.GroupTitle == chapter);
if (firstEntryInChapter != null)
{
OutlineStack.Children.Add(CreateOutlineButton(chapter, firstEntryInChapter, isHeader: true, isNested: false));
}
}
}
else
{
foreach (ConfessionEntry entry in document.Entries)
{
string title = entry.NavigationTitle;
OutlineStack.Children.Add(CreateOutlineButton(title, entry, isHeader: false, isNested: false));
}
}
}
private Button CreateOutlineButton(string title, ConfessionEntry entry, bool isHeader, bool isNested)
{
Button button = new()
{
Tag = entry,
Padding = new Thickness(isNested ? 32 : 12, isHeader ? 12 : 8, 12, isHeader ? 12 : 8),
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Stretch,
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(4),
Content = new TextBlock
{
Text = title,
FontWeight = isHeader ? FontWeights.SemiBold : FontWeights.Normal,
TextTrimming = TextTrimming.CharacterEllipsis,
TextWrapping = TextWrapping.NoWrap
}
};
button.Click += OutlineButton_Click;
return button;
}
private void OutlineButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button { Tag: ConfessionEntry entry })
{
ScrollToEntry(entry);
}
}
public void ScrollToEntry(ConfessionEntry entry)
{
double totalOffset = 0;
// Add the header offset (title + metadata)
totalOffset += 200; // Approximate header height
foreach (var child in EntryStack.Children)
{
if (child is FrameworkElement fe && fe.Tag == entry)
{
ReaderScrollViewer.ChangeView(0, totalOffset, null, true);
return;
}
if (child is FrameworkElement childFe)
{
totalOffset += childFe.ActualHeight + 16; // spacing
}
}
}
private FrameworkElement CreateEntryElement(ConfessionEntry entry)
{
StackPanel panel = new()
{
Spacing = 4,
Margin = new Thickness(0, 0, 0, 20),
Tag = entry
};
if (entry.ProofReferences.Count > 0)
{
AddContentWithFootnotes(panel, entry.Content, entry.ProofReferences);
}
else
{
AddContentParagraphs(panel, entry.Content);
}
return panel;
}
private void AddContentWithFootnotes(StackPanel panel, string content, IReadOnlyList<string> proofs)
{
// Just display the content - proofs are available via the expander toggle
foreach (string paragraph in SplitParagraphs(content))
{
TextBlock textBlock = new()
{
FontFamily = new FontFamily(AppState.ReaderFontFamily),
FontSize = AppState.ReaderFontSize,
LineHeight = AppState.ReaderFontSize * AppState.LineSpacing,
TextWrapping = TextWrapping.WrapWholeWords,
Text = paragraph
};
panel.Children.Add(textBlock);
}
}
private static TextBlock CreateTextBlock(string text)
{
return new TextBlock
{
Text = text,
FontFamily = new FontFamily(AppState.ReaderFontFamily),
FontSize = AppState.ReaderFontSize,
LineHeight = AppState.ReaderFontSize * AppState.LineSpacing,
TextWrapping = TextWrapping.WrapWholeWords
};
}
private static void AddContentParagraphs(StackPanel panel, string content)
{
foreach (string paragraph in SplitParagraphs(content))
{
panel.Children.Add(CreateTextBlock(paragraph));
}
}
private static IEnumerable<string> SplitParagraphs(string content)
{
return content
.ReplaceLineEndings("\n")
.Split("\n\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(paragraph => !string.IsNullOrWhiteSpace(paragraph));
}
private static Expander CreateProofsExpander(ConfessionEntry entry)
{
StackPanel referencesPanel = new()
{
Spacing = 8,
Margin = new Thickness(0, 8, 0, 0)
};
foreach (string reference in entry.ProofReferences)
{
referencesPanel.Children.Add(new TextBlock
{
Text = reference,
Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"],
Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"],
TextWrapping = TextWrapping.WrapWholeWords
});
}
return new Expander
{
Header = entry.ProofReferences.Count == 1 ? "1 scripture proof" : $"{entry.ProofReferences.Count} scripture proofs",
Content = referencesPanel,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Stretch
};
}
private static IReadOnlyList<string> BuildMetadata(ConfessionDocument document)
{
return new[] { document.DisplayFormat, document.Year, document.Authors, document.Location, document.EntrySummary }
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToList();
}
private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
{
bool narrow = e.NewSize.Width < 640;
OutlineColumn.Width = new GridLength(narrow ? 0 : 300);
OutlinePane.Visibility = narrow ? Visibility.Collapsed : Visibility.Visible;
ReaderScrollViewer.Padding = narrow ? new Thickness(24, 16, 24, 24) : new Thickness(48, 24, 48, 48);
}
}
+103
View File
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<Page
x:Class="Confessio.Views.SettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Confessio"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ScrollViewer Padding="36,24,36,36">
<StackPanel MaxWidth="600" Spacing="24">
<TextBlock Text="Settings"
Style="{StaticResource TitleTextBlockStyle}"
AutomationProperties.HeadingLevel="Level1" />
<StackPanel Spacing="16">
<TextBlock Text="About"
Style="{StaticResource BodyStrongTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<Border Style="{StaticResource LibraryCardBorderStyle}" Padding="16">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="12">
<Border Width="48"
Height="48"
CornerRadius="8"
Background="{ThemeResource AccentFillColorSecondaryBrush}">
<FontIcon Glyph="&#xE8A5;"
FontSize="24"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Border>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Confessio"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<TextBlock x:Name="VersionText"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</StackPanel>
<TextBlock Text="A digital library of Reformed and Presbyterian confessions, including the Westminster Confession of Faith, the Scots Confession, the Heidelberg Catechism, and more."
Style="{StaticResource BodyTextBlockStyle}"
TextWrapping="Wrap"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="Powered by:"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorTertiaryBrush}" />
<TextBlock Text="Windows App SDK, .NET 8, WinUI 3"
Style="{StaticResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Data"
Style="{StaticResource BodyStrongTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<Border Style="{StaticResource LibraryCardBorderStyle}" Padding="16">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Documents loaded:"
Style="{StaticResource BodyTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="DocumentCountText"
Style="{StaticResource BodyTextBlockStyle}"
Margin="8,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Collections:"
Style="{StaticResource BodyTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="CollectionCountText"
Style="{StaticResource BodyTextBlockStyle}"
Margin="8,0,0,0" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Legal"
Style="{StaticResource BodyStrongTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<Border Style="{StaticResource LibraryCardBorderStyle}" Padding="16">
<TextBlock Text="Confessio is an independent project and is not affiliated with any denomination. The confession texts are in the public domain or used under fair use. This software is provided as-is."
Style="{StaticResource BodyTextBlockStyle}"
TextWrapping="Wrap"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</Border>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Page>
+19
View File
@@ -0,0 +1,19 @@
using Microsoft.UI.Xaml.Controls;
namespace Confessio.Views;
public sealed partial class SettingsPage : Page
{
public SettingsPage()
{
InitializeComponent();
// Get app version from assembly
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
VersionText.Text = version != null ? $"Version {version.Major}.{version.Minor}.{version.Build}" : "Version 1.0";
// Display library stats
DocumentCountText.Text = AppState.AllDocuments.Count.ToString();
CollectionCountText.Text = AppState.Collections.Count.ToString();
}
}