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
89 changes: 70 additions & 19 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Window x:Class="Booky.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Booky"
Title="Booky · VoidMind"
Icon="Assets/book.ico"
Width="500"
Expand All @@ -16,6 +17,9 @@
DragOver="Window_DragOver">

<Window.Resources>
<!-- Converters -->
<local:NullToCollapsedConverter x:Key="NullToCollapsedConverter" />

<!-- Book icon path data -->
<Geometry x:Key="BookIcon">M19 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H19C20.1 22 21 21.1 21 20V4C21 2.9 20.1 2 19 2ZM6 4H11V12L8.5 10.5L6 12V4Z</Geometry>
<Geometry x:Key="SettingsIcon">M19.14 12.94C19.18 12.64 19.2 12.33 19.2 12C19.2 11.68 19.18 11.36 19.13 11.06L21.16 9.48C21.34 9.34 21.39 9.07 21.28 8.87L19.36 5.55C19.24 5.33 18.99 5.26 18.77 5.33L16.38 6.29C15.88 5.91 15.35 5.59 14.76 5.35L14.4 2.81C14.36 2.57 14.16 2.4 13.92 2.4H10.08C9.84 2.4 9.65 2.57 9.61 2.81L9.25 5.35C8.66 5.59 8.12 5.92 7.63 6.29L5.24 5.33C5.02 5.25 4.77 5.33 4.65 5.55L2.74 8.87C2.62 9.08 2.66 9.34 2.86 9.48L4.89 11.06C4.84 11.36 4.8 11.69 4.8 12C4.8 12.31 4.82 12.64 4.87 12.94L2.85 14.52C2.67 14.66 2.62 14.93 2.73 15.13L4.65 18.45C4.77 18.67 5.02 18.74 5.24 18.67L7.63 17.71C8.13 18.09 8.66 18.41 9.25 18.65L9.61 21.19C9.65 21.43 9.84 21.6 10.08 21.6H13.92C14.16 21.6 14.36 21.43 14.39 21.19L14.75 18.65C15.34 18.41 15.88 18.09 16.37 17.71L18.76 18.67C18.98 18.75 19.23 18.67 19.35 18.45L21.27 15.13C21.39 14.91 21.34 14.66 21.15 14.52L19.14 12.94ZM12 15.6C10.02 15.6 8.4 13.98 8.4 12C8.4 10.02 10.02 8.4 12 8.4C13.98 8.4 15.6 10.02 15.6 12C15.6 13.98 13.98 15.6 12 15.6Z</Geometry>
Expand Down Expand Up @@ -91,7 +95,7 @@
Foreground="{StaticResource TextBrush}"
HorizontalAlignment="Center"
Margin="0,0,0,8" />
<TextBlock Text="MOBI files"
<TextBlock Text="MOBI &amp; EPUB files"
FontSize="14"
Foreground="{StaticResource TextMutedBrush}"
HorizontalAlignment="Center"
Expand Down Expand Up @@ -136,20 +140,49 @@
Stretch="Uniform" />
</Button>

<!-- File info -->
<StackPanel Grid.Row="1" Margin="0,0,0,24">
<TextBlock x:Name="FileNameText"
Text="book.mobi"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextBrush}"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="FileTypeText"
Text="MOBI File"
FontSize="13"
Foreground="{StaticResource TextMutedBrush}"
Margin="0,4,0,0" />
</StackPanel>
<!-- File info with cover -->
<Grid Grid.Row="1" Margin="0,0,0,24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<!-- Cover image -->
<Border x:Name="CoverBorder"
Grid.Column="0"
Width="80"
Height="120"
Margin="0,0,16,0"
CornerRadius="4"
Background="{StaticResource DropZoneBrush}"
Visibility="Collapsed">
<Border.Effect>
<DropShadowEffect BlurRadius="8" Opacity="0.15" ShadowDepth="2" Direction="270" />
</Border.Effect>
<Image x:Name="CoverImage"
Stretch="UniformToFill"
RenderOptions.BitmapScalingMode="HighQuality">
<Image.Clip>
<RectangleGeometry Rect="0,0,80,120" RadiusX="4" RadiusY="4" />
</Image.Clip>
</Image>
</Border>

<!-- File info text -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock x:Name="FileNameText"
Text="book.mobi"
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource TextBrush}"
TextTrimming="CharacterEllipsis" />
<TextBlock x:Name="FileTypeText"
Text="MOBI File"
FontSize="13"
Foreground="{StaticResource TextMutedBrush}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>

<!-- Editable metadata -->
<StackPanel Grid.Row="2">
Expand Down Expand Up @@ -290,21 +323,39 @@
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
<ColumnDefinition Width="70" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"

<!-- Small cover thumbnail -->
<Border Grid.Column="0"
Width="32" Height="48"
Margin="0,0,12,0"
CornerRadius="2"
Background="{StaticResource DropZoneBrush}"
Visibility="{Binding CoverImage, Converter={StaticResource NullToCollapsedConverter}}">
<Image Source="{Binding CoverImage}"
Stretch="UniformToFill"
RenderOptions.BitmapScalingMode="HighQuality">
<Image.Clip>
<RectangleGeometry Rect="0,0,32,48" RadiusX="2" RadiusY="2" />
</Image.Clip>
</Image>
</Border>

<TextBox Grid.Column="1"
Text="{Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource ModernTextBox}"
Margin="0,0,8,0"
ToolTip="Title" />
<TextBox Grid.Column="1"
<TextBox Grid.Column="2"
Text="{Binding Author, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource ModernTextBox}"
Margin="0,0,8,0"
ToolTip="Author" />
<TextBlock Grid.Column="2"
<TextBlock Grid.Column="3"
Text="{Binding Status}"
Foreground="{StaticResource TextMutedBrush}"
VerticalAlignment="Center"
Expand Down
52 changes: 51 additions & 1 deletion MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ private async void LoadFile(string filePath)
MultiFilePanel.Visibility = Visibility.Collapsed;
FilePanel.Visibility = Visibility.Visible;

// Reset cover
CoverImage.Source = null;
CoverBorder.Visibility = Visibility.Collapsed;

SetStatus($"Loading: {fileName}...");

if (extension == ".epub")
Expand All @@ -242,6 +246,20 @@ private async void LoadFile(string filePath)
// Keep filename-based title
}

// Extract cover (run on background thread)
_ = Task.Run(() => _conversionService.ExtractEpubCover(filePath))
.ContinueWith(t =>
{
if (t.Result != null)
{
Dispatcher.Invoke(() =>
{
CoverImage.Source = t.Result;
CoverBorder.Visibility = Visibility.Visible;
});
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Comment on lines +250 to +261

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fire-and-forget tasks can cause exceptions to be silently swallowed and make it difficult to debug issues. Additionally, if these tasks are still running when the window is closed or a new file is loaded, they may try to update UI elements that no longer exist or are bound to different data. Consider storing task references and cancelling them when appropriate, or at minimum using proper exception handling in the continuation.

Copilot uses AI. Check for mistakes.

// Store for Kindle sending (the EPUB itself is the output)
_lastOutputPath = filePath;
_lastTitle = TitleTextBox.Text;
Expand Down Expand Up @@ -273,6 +291,20 @@ private async void LoadFile(string filePath)
{
SetStatus($"Loaded: {fileName}");
}

// Extract cover (run on background thread)
_ = Task.Run(() => _conversionService.ExtractMobiCover(filePath))
.ContinueWith(t =>
{
if (t.Result != null)
{
Dispatcher.Invoke(() =>
{
CoverImage.Source = t.Result;
CoverBorder.Visibility = Visibility.Visible;
});
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Comment on lines +296 to +307

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fire-and-forget tasks can cause exceptions to be silently swallowed and make it difficult to debug issues. Additionally, if these tasks are still running when the window is closed or a new file is loaded, they may try to update UI elements that no longer exist or are bound to different data. Consider storing task references and cancelling them when appropriate, or at minimum using proper exception handling in the continuation.

Suggested change
_ = Task.Run(() => _conversionService.ExtractMobiCover(filePath))
.ContinueWith(t =>
{
if (t.Result != null)
{
Dispatcher.Invoke(() =>
{
CoverImage.Source = t.Result;
CoverBorder.Visibility = Visibility.Visible;
});
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
var coverTask = Task.Run(() => _conversionService.ExtractMobiCover(filePath));
coverTask.ContinueWith(t =>
{
var cover = t.Result;
if (cover != null)
{
Dispatcher.Invoke(() =>
{
CoverImage.Source = cover;
CoverBorder.Visibility = Visibility.Visible;
});
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
coverTask.ContinueWith(t =>
{
// Observe and handle exceptions to avoid unobserved task exceptions
var _ = t.Exception;
Dispatcher.Invoke(() =>
{
SetStatus("Failed to extract cover from MOBI file", isError: true);
});
}, TaskContinuationOptions.OnlyOnFaulted);

Copilot uses AI. Check for mistakes.
}
else
{
Expand Down Expand Up @@ -325,7 +357,7 @@ private async void LoadMultipleFiles(string[] filePaths)
}
}

// Extract metadata for each file
// Extract metadata and covers for each file
foreach (var book in _books)
{
try
Expand All @@ -337,6 +369,15 @@ private async void LoadMultipleFiles(string[] filePaths)
book.Title = metadata.Title;
if (!string.IsNullOrEmpty(metadata.Author))
book.Author = metadata.Author;

// Extract cover
var filePath = book.FilePath;
_ = Task.Run(() => _conversionService.ExtractEpubCover(filePath!))
.ContinueWith(t =>
{
if (t.Result != null)
Dispatcher.Invoke(() => book.CoverImage = t.Result);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Comment on lines +375 to +380

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fire-and-forget tasks can cause exceptions to be silently swallowed and make it difficult to debug issues. Additionally, if these tasks are still running when files are reloaded or the window is closed, they may try to update BookMetadata objects that are no longer in the collection. Consider storing task references and cancelling them when appropriate, or at minimum using proper exception handling in the continuation.

Copilot uses AI. Check for mistakes.
}
else
{
Expand All @@ -345,6 +386,15 @@ private async void LoadMultipleFiles(string[] filePaths)
book.Title = metadata.Title;
if (!string.IsNullOrEmpty(metadata.Author))
book.Author = metadata.Author;

// Extract cover
var filePath = book.FilePath;
_ = Task.Run(() => _conversionService.ExtractMobiCover(filePath!))
.ContinueWith(t =>
{
if (t.Result != null)
Dispatcher.Invoke(() => book.CoverImage = t.Result);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Comment on lines +392 to +397

Copilot AI Jan 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fire-and-forget tasks can cause exceptions to be silently swallowed and make it difficult to debug issues. Additionally, if these tasks are still running when files are reloaded or the window is closed, they may try to update BookMetadata objects that are no longer in the collection. Consider storing task references and cancelling them when appropriate, or at minimum using proper exception handling in the continuation.

Copilot uses AI. Check for mistakes.
}
}
catch
Expand Down
8 changes: 8 additions & 0 deletions Models/BookMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Windows.Media.Imaging;

namespace Booky.Models;

Expand All @@ -10,6 +11,7 @@ public class BookMetadata : INotifyPropertyChanged
private string? _fileName;
private string? _status;
private bool _isEpub;
private BitmapImage? _coverImage;

public string? Title
{
Expand Down Expand Up @@ -47,6 +49,12 @@ public bool IsEpub
set { _isEpub = value; OnPropertyChanged(nameof(IsEpub)); }
}

public BitmapImage? CoverImage
{
get => _coverImage;
set { _coverImage = value; OnPropertyChanged(nameof(CoverImage)); }
}

public event PropertyChangedEventHandler? PropertyChanged;

protected void OnPropertyChanged(string propertyName)
Expand Down
18 changes: 18 additions & 0 deletions NullToCollapsedConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace Booky;

public class NullToCollapsedConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value == null ? Visibility.Collapsed : Visibility.Visible;
}

public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Loading