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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: ci

on:
push:
branches: [master]
branches: ['**']
pull_request:
branches: [master]

Expand Down
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 4: Authoring + parameter-fill flyout
- Snip card actions are now live: **Copy** opens a parameter-fill
`ContentDialog` (or copies the template directly when the Snip has no
parameters), **Edit** opens a Snip editor, **Delete** soft-trashes after
confirmation, the **star** toggles favourite.
- Copying a Snip bumps `UsageCount` and `LastUsedAt` (drives the most-used
list on Home).
- New Snip / New CLI buttons on Home and the CLI view. Edit CLI button on the
CLI view header.
- `SnipEditorDialog` — title, command template (monospace, multi-line),
description, tags (comma-separated), parameter rows with type
(Text / Choice), default, and options. Add/remove parameters inline.
- `CliEditorDialog` — name + icon picker. Picked images are normalised to a
256² centre-square PNG via `WindowsIconNormaliser` (`Windows.Graphics.Imaging`)
and stored under `<data>/icons/<cli-id>.png` by `IconAssetStorage`.
- `ParameterFillDialog` — one input per parameter (TextBox for `Text`,
ComboBox for `Choice`), with a live preview of the resolved command and
the Copy button disabled until the template is fully resolved.
- New Core abstractions: `IClipboardService`, `IIconNormaliser`,
`IIconAssetStorage`, `IShellInteractions`.
- New Core view models: `ParameterFillViewModel`, `ParameterInputViewModel`,
`SnipEditorViewModel`, `ParameterEditorRowViewModel`, `CliEditorViewModel`.
- `ShellViewModel` gains `CopySnipCommand`, `EditSnipCommand`,
`DeleteSnipCommand`, `ToggleFavouriteCommand`, `NewSnipCommand`,
`NewCliCommand`, `EditCurrentCliCommand`, `SelectCliCommand`.
- App-side implementations: `WindowsClipboardService`,
`WindowsIconNormaliser`, `WindowsShellInteractions`.
- Clicking a CLI card on Home navigates into that CLI (was: switcher-only).
- 18 new Core unit tests cover the new view models and the command flow
(clipboard write, usage bumping, soft-delete, favourite toggle,
new-CLI-with-icon).

### 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
Expand Down
5 changes: 5 additions & 0 deletions src/Snipdeck.App/Bootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,20 @@ public static IServiceProvider Build()

var snipStore = new JsonSnipStore(snipStoreFilePath);
var backupService = new BackupService(snipStoreFilePath, backupDirectory, clock);
var iconStorage = new IconAssetStorage(storageDirectory);

var services = new ServiceCollection();
_ = services
.AddSingleton<IPathProvider>(pathProvider)
.AddSingleton<IClock>(clock)
.AddSingleton<IDispatcher, WinUiDispatcher>()
.AddSingleton<IClipboardService, WindowsClipboardService>()
.AddSingleton<IIconNormaliser, WindowsIconNormaliser>()
.AddSingleton<IShellInteractions, WindowsShellInteractions>()
.AddSingleton<ISettingsStore>(settingsStore)
.AddSingleton<ISnipStore>(snipStore)
.AddSingleton<IBackupService>(backupService)
.AddSingleton<IIconAssetStorage>(iconStorage)
.AddSingleton(config)
.AddSingleton<ShellViewModel>()
.AddSingleton<ShellPage>()
Expand Down
3 changes: 2 additions & 1 deletion src/Snipdeck.App/Controls/CliCard.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
CornerRadius="8"
Padding="16"
Width="200"
Height="200">
Height="200"
Tapped="OnTapped">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
Expand Down
26 changes: 22 additions & 4 deletions src/Snipdeck.App/Controls/CliCard.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Windows.Input;

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;

using Snipdeck.Core.ViewModels;

Expand All @@ -8,10 +11,11 @@ namespace Snipdeck.App.Controls
public sealed partial class CliCard : UserControl
{
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(
nameof(ViewModel),
typeof(CliCardViewModel),
typeof(CliCard),
DependencyProperty.Register(nameof(ViewModel), typeof(CliCardViewModel), typeof(CliCard),
new PropertyMetadata(null));

public static readonly DependencyProperty NavigateCommandProperty =
DependencyProperty.Register(nameof(NavigateCommand), typeof(ICommand), typeof(CliCard),
new PropertyMetadata(null));

public CliCard()
Expand All @@ -24,5 +28,19 @@ public CliCardViewModel? ViewModel
get => (CliCardViewModel?)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}

public ICommand? NavigateCommand
{
get => (ICommand?)GetValue(NavigateCommandProperty);
set => SetValue(NavigateCommandProperty, value);
}

private void OnTapped(object sender, TappedRoutedEventArgs e)
{
if (NavigateCommand?.CanExecute(ViewModel) == true)
{
NavigateCommand.Execute(ViewModel);
}
}
}
}
37 changes: 28 additions & 9 deletions src/Snipdeck.App/Controls/SnipCard.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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:local="using:Snipdeck.App.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
Expand Down Expand Up @@ -36,12 +37,18 @@
Style="{ThemeResource BodyStrongTextBlockStyle}"
TextTrimming="CharacterEllipsis" />

<FontIcon
<Button
Grid.Column="1"
Glyph="&#xE735;"
FontSize="14"
Margin="8,0,0,0"
Visibility="{x:Bind ViewModel.IsFavourite, Mode=OneWay, Converter={StaticResource BoolToVisibility}}" />
Padding="6"
Background="Transparent"
BorderThickness="0"
Command="{x:Bind FavouriteCommand, Mode=OneWay}"
CommandParameter="{x:Bind ViewModel, Mode=OneWay}"
ToolTipService.ToolTip="Toggle favourite">
<FontIcon Glyph="{x:Bind local:SnipCard.FavouriteGlyph(ViewModel.IsFavourite), Mode=OneWay}"
FontSize="14" />
</Button>
</Grid>

<Border
Expand Down Expand Up @@ -82,21 +89,33 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<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."
Style="{ThemeResource AccentButtonStyle}"
Command="{x:Bind CopyCommand, Mode=OneWay}"
CommandParameter="{x:Bind ViewModel, Mode=OneWay}"
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" />
ToolTipService.ToolTip="Edit"
Command="{x:Bind EditCommand, Mode=OneWay}"
CommandParameter="{x:Bind ViewModel, Mode=OneWay}">
<FontIcon Glyph="&#xE70F;" FontSize="14" />
</Button>

<Button
Grid.Column="2"
Margin="8,0,0,0"
ToolTipService.ToolTip="Delete"
Command="{x:Bind DeleteCommand, Mode=OneWay}"
CommandParameter="{x:Bind ViewModel, Mode=OneWay}">
<FontIcon Glyph="&#xE74D;" FontSize="14" />
</Button>
</Grid>
</Grid>
Expand Down
49 changes: 45 additions & 4 deletions src/Snipdeck.App/Controls/SnipCard.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Windows.Input;

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

Expand All @@ -8,10 +10,23 @@ namespace Snipdeck.App.Controls
public sealed partial class SnipCard : UserControl
{
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(
nameof(ViewModel),
typeof(SnipCardViewModel),
typeof(SnipCard),
DependencyProperty.Register(nameof(ViewModel), typeof(SnipCardViewModel), typeof(SnipCard),
new PropertyMetadata(null));

public static readonly DependencyProperty CopyCommandProperty =
DependencyProperty.Register(nameof(CopyCommand), typeof(ICommand), typeof(SnipCard),
new PropertyMetadata(null));

public static readonly DependencyProperty EditCommandProperty =
DependencyProperty.Register(nameof(EditCommand), typeof(ICommand), typeof(SnipCard),
new PropertyMetadata(null));

public static readonly DependencyProperty DeleteCommandProperty =
DependencyProperty.Register(nameof(DeleteCommand), typeof(ICommand), typeof(SnipCard),
new PropertyMetadata(null));

public static readonly DependencyProperty FavouriteCommandProperty =
DependencyProperty.Register(nameof(FavouriteCommand), typeof(ICommand), typeof(SnipCard),
new PropertyMetadata(null));

public SnipCard()
Expand All @@ -24,5 +39,31 @@ public SnipCardViewModel? ViewModel
get => (SnipCardViewModel?)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}

public ICommand? CopyCommand
{
get => (ICommand?)GetValue(CopyCommandProperty);
set => SetValue(CopyCommandProperty, value);
}

public ICommand? EditCommand
{
get => (ICommand?)GetValue(EditCommandProperty);
set => SetValue(EditCommandProperty, value);
}

public ICommand? DeleteCommand
{
get => (ICommand?)GetValue(DeleteCommandProperty);
set => SetValue(DeleteCommandProperty, value);
}

public ICommand? FavouriteCommand
{
get => (ICommand?)GetValue(FavouriteCommandProperty);
set => SetValue(FavouriteCommandProperty, value);
}

public static string FavouriteGlyph(bool isFavourite) => isFavourite ? "\uE735" : "\uE734";
}
}
18 changes: 18 additions & 0 deletions src/Snipdeck.App/Services/WindowsClipboardService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Snipdeck.Core.Abstractions;

using Windows.ApplicationModel.DataTransfer;

namespace Snipdeck.App.Services
{
internal sealed class WindowsClipboardService : IClipboardService
{
public Task SetTextAsync(string text)
{
ArgumentNullException.ThrowIfNull(text);
var package = new DataPackage();
package.SetText(text);
Clipboard.SetContent(package);
return Task.CompletedTask;
}
}
}
81 changes: 81 additions & 0 deletions src/Snipdeck.App/Services/WindowsIconNormaliser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Snipdeck.Core.Abstractions;

using Windows.Graphics.Imaging;
using Windows.Storage.Streams;

namespace Snipdeck.App.Services
{
/// <summary>
/// Centre-crops, resizes to <c>maxEdgePixels</c>, and re-encodes the
/// source image as PNG. Implemented with <c>Windows.Graphics.Imaging</c>
/// so we don't pull in heavier image libraries.
/// </summary>
internal sealed class WindowsIconNormaliser : IIconNormaliser
{
public async Task<byte[]> NormaliseAsync(byte[] sourceBytes, int maxEdgePixels = 256)
{
ArgumentNullException.ThrowIfNull(sourceBytes);
if (maxEdgePixels < 16)
{
throw new ArgumentOutOfRangeException(nameof(maxEdgePixels), maxEdgePixels, "Edge size must be at least 16 pixels.");
}

using var sourceStream = new InMemoryRandomAccessStream();
var writer = new DataWriter(sourceStream);
writer.WriteBytes(sourceBytes);
_ = await writer.StoreAsync();
_ = writer.DetachStream();
sourceStream.Seek(0);

var decoder = await BitmapDecoder.CreateAsync(sourceStream);

var sourceWidth = decoder.PixelWidth;
var sourceHeight = decoder.PixelHeight;
var edge = Math.Min(sourceWidth, sourceHeight);
var offsetX = (sourceWidth - edge) / 2u;
var offsetY = (sourceHeight - edge) / 2u;
var targetEdge = Math.Min((uint)maxEdgePixels, edge);

var transform = new BitmapTransform
{
Bounds = new BitmapBounds
{
X = offsetX,
Y = offsetY,
Width = edge,
Height = edge,
},
ScaledWidth = targetEdge,
ScaledHeight = targetEdge,
InterpolationMode = BitmapInterpolationMode.Fant,
};

var pixelData = await decoder.GetPixelDataAsync(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied,
transform,
ExifOrientationMode.RespectExifOrientation,
ColorManagementMode.DoNotColorManage);

using var outputStream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, outputStream);
encoder.SetPixelData(
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied,
targetEdge,
targetEdge,
decoder.DpiX,
decoder.DpiY,
pixelData.DetachPixelData());
await encoder.FlushAsync();

outputStream.Seek(0);
var reader = new DataReader(outputStream.GetInputStreamAt(0));
var length = (uint)outputStream.Size;
_ = await reader.LoadAsync(length);
var bytes = new byte[length];
reader.ReadBytes(bytes);
return bytes;
}
}
}
Loading
Loading