Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added — Phase 3: Shell + read-only browse
- `ShellViewModel` owns the cross-cutting shell state: CLI switcher choices,
current search text, selected tag (with an "All" sentinel for clean
filter-off semantics), and the active content view model.
- `HomeViewModel` builds the home view's CLI cards (alphabetical, with snip
counts) and the most-used Snips list (top 6 by `UsageCount`, then
`LastUsedAt` desc; hidden when nothing's been used yet).
- `CliViewModel` exposes the filtered Snip list for a single CLI, favourites
bubbled to the top.
- `SettingsViewModel` stub — populates the About expander; real settings UI
arrives in Phase 6.
- `SnipFilter` pure helpers: case-insensitive search across title / template /
tags, tag filter, trash exclusion, `DistinctTagsFor` for the pane tag list.
- `IdenticonService` (Jdenticon-net) — generates deterministic identicon PNG
bytes seeded off `Cli.Id` so renaming a CLI doesn't change its icon.
- `ShellPage` (WinUI): `NavigationView` with a custom pane header
(`AutoSuggestBox` search + CLI switcher `ComboBox`), pane body tag list,
pane footer Settings button, content area driven by a
`ShellContentTemplateSelector` that picks the right `DataTemplate` based on
the current content view-model type.
- Custom user controls: `Identicon` (dependency-property-driven, lazy image
load), `CliCard` (identicon + name + snip count), `SnipCard` (title,
monospace template preview, tag chips, favourite star, disabled
Copy/Edit/Delete buttons with "Phase 4/5" tooltips).
- Settings page stub uses `SettingsCard` / `SettingsExpander` with About as the
last expander; About shows app name, copyright, and version (the version
string falls back to the assembly's `InformationalVersion` until Phase 6
wires Nerdbank.GitVersioning).
- `MainWindow` now hosts the `ShellPage` in its content row; the custom title
bar and Mica backdrop carry over from Phase 2.
- Converters: `BoolToVisibilityConverter`, `CountToVisibilityConverter`.

### Added — Phase 2: App lifecycle skeleton
- Explicit `Program.cs` entry point that runs the Velopack hook → initialises
WinRT COM wrappers → checks single-instance via Windows App SDK
Expand Down
4 changes: 4 additions & 0 deletions src/Snipdeck.App/Bootstrap.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Microsoft.Extensions.DependencyInjection;

using Snipdeck.App.Services;
using Snipdeck.App.Views;
using Snipdeck.Core.Abstractions;
using Snipdeck.Core.Models;
using Snipdeck.Core.Services;
using Snipdeck.Core.ViewModels;

namespace Snipdeck.App
{
Expand Down Expand Up @@ -40,6 +42,8 @@ public static IServiceProvider Build()
.AddSingleton<ISnipStore>(snipStore)
.AddSingleton<IBackupService>(backupService)
.AddSingleton(config)
.AddSingleton<ShellViewModel>()
.AddSingleton<ShellPage>()
.AddSingleton<MainWindow>();

return services.BuildServiceProvider();
Expand Down
49 changes: 49 additions & 0 deletions src/Snipdeck.App/Controls/CliCard.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Snipdeck.App.Controls.CliCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Snipdeck.App.Controls"
xmlns:vm="using:Snipdeck.Core.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16"
Width="200"
Height="200">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<controls:Identicon
Grid.Row="0"
Seed="{x:Bind ViewModel.Id, Mode=OneWay}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Width="96"
Height="96" />

<TextBlock
Grid.Row="1"
Text="{x:Bind ViewModel.Name, Mode=OneWay}"
Style="{ThemeResource BodyStrongTextBlockStyle}"
HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis"
Margin="0,12,0,0" />

<TextBlock
Grid.Row="2"
Text="{x:Bind ViewModel.SnipCountDisplay, Mode=OneWay}"
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
HorizontalAlignment="Center"
Margin="0,2,0,0" />
</Grid>
</UserControl>
28 changes: 28 additions & 0 deletions src/Snipdeck.App/Controls/CliCard.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

using Snipdeck.Core.ViewModels;

namespace Snipdeck.App.Controls
{
public sealed partial class CliCard : UserControl
{
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(
nameof(ViewModel),
typeof(CliCardViewModel),
typeof(CliCard),
new PropertyMetadata(null));

public CliCard()
{
InitializeComponent();
}

public CliCardViewModel? ViewModel
{
get => (CliCardViewModel?)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
}
}
14 changes: 14 additions & 0 deletions src/Snipdeck.App/Controls/Identicon.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Snipdeck.App.Controls.Identicon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Image x:Name="IconImage"
Stretch="Uniform"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</UserControl>
63 changes: 63 additions & 0 deletions src/Snipdeck.App/Controls/Identicon.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;

using Snipdeck.Core.Services;

using Windows.Storage.Streams;

namespace Snipdeck.App.Controls
{
/// <summary>
/// Renders an identicon from a <see cref="Guid"/> seed. The seed should be
/// the immutable <c>Cli.Id</c> so renaming a CLI doesn't change its icon.
/// </summary>
public sealed partial class Identicon : UserControl
{
public static readonly DependencyProperty SeedProperty =
DependencyProperty.Register(
nameof(Seed),
typeof(Guid),
typeof(Identicon),
new PropertyMetadata(Guid.Empty, OnSeedChanged));

public Identicon()
{
InitializeComponent();
}

public Guid Seed
{
get => (Guid)GetValue(SeedProperty);
set => SetValue(SeedProperty, value);
}

private static void OnSeedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Identicon icon)
{
_ = icon.UpdateImageAsync();
}
}

private async Task UpdateImageAsync()
{
if (Seed == Guid.Empty)
{
IconImage.Source = null;
return;
}

var bytes = IdenticonService.GeneratePng(Seed);
var image = new BitmapImage();
using var stream = new InMemoryRandomAccessStream();
var writer = new DataWriter(stream);
writer.WriteBytes(bytes);
_ = await writer.StoreAsync();
_ = writer.DetachStream();
stream.Seek(0);
await image.SetSourceAsync(stream);
IconImage.Source = image;
}
}
}
103 changes: 103 additions & 0 deletions src/Snipdeck.App/Controls/SnipCard.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Snipdeck.App.Controls.SnipCard"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Snipdeck.App.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<UserControl.Resources>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibility" />
</UserControl.Resources>

<Grid Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<TextBlock
Grid.Column="0"
Text="{x:Bind ViewModel.Title, Mode=OneWay}"
Style="{ThemeResource BodyStrongTextBlockStyle}"
TextTrimming="CharacterEllipsis" />

<FontIcon
Grid.Column="1"
Glyph="&#xE735;"
FontSize="14"
Margin="8,0,0,0"
Visibility="{x:Bind ViewModel.IsFavourite, Mode=OneWay, Converter={StaticResource BoolToVisibility}}" />
</Grid>

<Border
Grid.Row="1"
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
CornerRadius="4"
Padding="8,6"
Margin="0,8,0,0">
<TextBlock
Text="{x:Bind ViewModel.CommandTemplate, Mode=OneWay}"
FontFamily="Cascadia Mono, Consolas, Courier New"
FontSize="12"
TextWrapping="Wrap" />
</Border>

<ItemsControl
Grid.Row="2"
ItemsSource="{x:Bind ViewModel.Tags, Mode=OneWay}"
Margin="0,8,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="6" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<Border Background="{ThemeResource AccentFillColorSecondaryBrush}"
CornerRadius="10"
Padding="8,2">
<TextBlock Text="{x:Bind}"
Style="{ThemeResource CaptionTextBlockStyle}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<Grid Grid.Row="3" Margin="0,12,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<Button
Grid.Column="0"
Content="Copy"
IsEnabled="False"
ToolTipService.ToolTip="Available in a future phase — parameter-fill flyout lands in Phase 4."
HorizontalAlignment="Stretch" />

<Button
Grid.Column="1"
Margin="8,0,0,0"
IsEnabled="False"
ToolTipService.ToolTip="Edit / Delete available in a future phase.">
<FontIcon Glyph="&#xE712;" FontSize="14" />
</Button>
</Grid>
</Grid>
</UserControl>
28 changes: 28 additions & 0 deletions src/Snipdeck.App/Controls/SnipCard.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

using Snipdeck.Core.ViewModels;

namespace Snipdeck.App.Controls
{
public sealed partial class SnipCard : UserControl
{
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(
nameof(ViewModel),
typeof(SnipCardViewModel),
typeof(SnipCard),
new PropertyMetadata(null));

public SnipCard()
{
InitializeComponent();
}

public SnipCardViewModel? ViewModel
{
get => (SnipCardViewModel?)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
}
}
25 changes: 25 additions & 0 deletions src/Snipdeck.App/Converters/BoolToVisibilityConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;

namespace Snipdeck.App.Converters
{
public sealed partial class BoolToVisibilityConverter : IValueConverter
{
public bool Invert { get; set; }

public object Convert(object value, Type targetType, object parameter, string language)
{
var flag = value is bool b && b;
if (Invert)
{
flag = !flag;
}
return flag ? Visibility.Visible : Visibility.Collapsed;
}

public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
}
Loading
Loading