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
1 change: 1 addition & 0 deletions src/BiSheng.Latte/Bisheng.Latte.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DiffPlex" Version="1.7.2" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
<PackageReference Include="Markdig" Version="0.41.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
Expand Down
11 changes: 10 additions & 1 deletion src/BiSheng.Latte/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,16 @@ private void OnToolbarDrag(object sender, MouseButtonEventArgs e)

bool? IMainWindowHost.ShowConflictDialog()
{
return new ConflictResolutionDialog(_vm.SyncEngine) { Owner = this }.ShowDialog();
try
{
return new ConflictResolutionDialog(_vm.SyncEngine) { Owner = this }.ShowDialog();
}
catch (Exception ex)
{
LogHelper.Error("打开冲突解决对话框失败", ex);
AppDialog.Error($"无法打开冲突解决窗口:\n{ex.Message}", "冲突解决");
return false;
}
}

void IMainWindowHost.ShowBackupManagerDialog() =>
Expand Down
37 changes: 37 additions & 0 deletions src/BiSheng.Latte/Models/ConflictDiffLine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace BiSheng.Latte.Models;

/// <summary>冲突 Diff 行类型</summary>
public enum ConflictDiffKind
{
/// <summary>两侧相同</summary>
Equal,

/// <summary>仅本地有(相对远端为删除)</summary>
Deleted,

/// <summary>仅远端有(相对本地为新增)</summary>
Inserted,
}

/// <summary>统一 Diff 视图的一行</summary>
public sealed class ConflictDiffLine
{
public ConflictDiffLine(ConflictDiffKind kind, string marker, string text)
{
Kind = kind;
Marker = marker;
Text = text;
}

/// <summary>变更类型</summary>
public ConflictDiffKind Kind { get; }

/// <summary>行首标记(空格 / - / +)</summary>
public string Marker { get; }

/// <summary>行文本(不含换行)</summary>
public string Text { get; }

/// <summary>展示用完整行</summary>
public string DisplayText => Marker == " " ? $" {Text}" : $"{Marker} {Text}";
}
40 changes: 40 additions & 0 deletions src/BiSheng.Latte/Services/ConflictDialogCopy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using BiSheng.Shared;

namespace BiSheng.Latte.Services;

/// <summary>冲突对话框文案(操作类型、删除场景按钮)</summary>
public static class ConflictDialogCopy
{
/// <summary>将 Create/Update/Delete 转为中文短标签</summary>
public static string FormatAction(string? action) => action switch
{
ChangeActions.Create => "创建",
ChangeActions.Update => "更新",
ChangeActions.Delete => "删除",
_ => string.IsNullOrWhiteSpace(action) ? "未知" : action
};

/// <summary>「保留本地」按钮文案</summary>
public static string KeepLocalButton(string? localAction) =>
localAction == ChangeActions.Delete ? "保留本机删除" : "保留本地版本";

/// <summary>「保留远端」按钮文案</summary>
public static string KeepRemoteButton(string? localAction, string? remoteAction)
{
if (remoteAction == ChangeActions.Delete)
{
return "采用远端删除";
}

if (localAction == ChangeActions.Delete)
{
return "恢复远端内容";
}

return "保留远端版本";
}

/// <summary>页眉操作对照说明</summary>
public static string FormatActionPair(string? localAction, string? remoteAction) =>
$"本地:{FormatAction(localAction)} · 远端:{FormatAction(remoteAction)}";
}
43 changes: 43 additions & 0 deletions src/BiSheng.Latte/Services/Diff/ConflictTextDiffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using BiSheng.Latte.Models;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;

namespace BiSheng.Latte.Services.Diff;

/// <summary>冲突正文行级 Diff(基于 DiffPlex)</summary>
public static class ConflictTextDiffer
{
/// <summary>
/// 以本地为旧、远端为新,生成统一 Diff 行列表。
/// Deleted = 仅本地有;Inserted = 仅远端有。
/// </summary>
public static IReadOnlyList<ConflictDiffLine> BuildUnified(string? localContent, string? remoteContent)
{
var diff = InlineDiffBuilder.Diff(localContent ?? string.Empty, remoteContent ?? string.Empty);
var lines = new List<ConflictDiffLine>(diff.Lines.Count);

foreach (var line in diff.Lines)
{
switch (line.Type)
{
case ChangeType.Deleted:
lines.Add(new ConflictDiffLine(ConflictDiffKind.Deleted, "-", line.Text));
break;
case ChangeType.Inserted:
lines.Add(new ConflictDiffLine(ConflictDiffKind.Inserted, "+", line.Text));
break;
case ChangeType.Modified:
// InlineDiffBuilder 通常拆成 Deleted+Inserted;若出现 Modified 按插入展示
lines.Add(new ConflictDiffLine(ConflictDiffKind.Inserted, "+", line.Text));
break;
case ChangeType.Imaginary:
break;
default:
lines.Add(new ConflictDiffLine(ConflictDiffKind.Equal, " ", line.Text));
break;
}
}

return lines;
}
}
11 changes: 7 additions & 4 deletions src/BiSheng.Latte/Services/Shell/DialogNavigationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,15 @@ public void OpenNoteHistory()
/// <inheritdoc />
public void OpenConflictDialog()
{
if (_getHost?.Invoke()?.ShowConflictDialog() != true)
try
{
return;
_ = _getHost?.Invoke()?.ShowConflictDialog();
}
finally
{
// 无论确定 / 取消 / 标题栏关闭,都按库内未解决数刷新徽章
_refreshConflictsAfterDialog?.Invoke();
}

_refreshConflictsAfterDialog?.Invoke();
}

/// <inheritdoc />
Expand Down
168 changes: 160 additions & 8 deletions src/BiSheng.Latte/Views/ConflictResolutionDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,54 @@
x:Class="BiSheng.Latte.Views.ConflictResolutionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:BiSheng.Latte.Models"
xmlns:search="clr-namespace:BiSheng.Latte.Controls.Search"
Title="同步冲突解决"
Width="1000"
Height="700"
MinWidth="800"
MinHeight="500"
Width="1100"
Height="740"
MinWidth="880"
MinHeight="560"
Background="{DynamicResource Brush.BgCanvas}"
WindowStartupLocation="CenterOwner">

<Window.Resources>
<DataTemplate x:Key="DiffLineTemplate" DataType="{x:Type models:ConflictDiffLine}">
<Border Padding="6,2" Background="Transparent">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding Kind}" Value="Deleted">
<Setter Property="Background" Value="#33C62828" />
</DataTrigger>
<DataTrigger Binding="{Binding Kind}" Value="Inserted">
<Setter Property="Background" Value="#332E7D32" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock
FontFamily="Consolas, Cascadia Mono, Courier New"
FontSize="12.5"
Text="{Binding DisplayText}"
TextWrapping="NoWrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Brush.Text}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Kind}" Value="Deleted">
<Setter Property="Foreground" Value="#B71C1C" />
</DataTrigger>
<DataTrigger Binding="{Binding Kind}" Value="Inserted">
<Setter Property="Foreground" Value="#1B5E20" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
</DataTemplate>
</Window.Resources>

<DockPanel>
<Border DockPanel.Dock="Top" Style="{StaticResource WindowHeaderBorder}">
<StackPanel>
Expand All @@ -18,11 +58,38 @@
x:Name="ConflictCountText"
Margin="0,4,0,0"
Style="{StaticResource WindowSubtitleText}"
Text="检测到 1 个未解决的冲突" />
Text="检测到冲突" />
<TextBlock
Margin="0,8,0,0"
x:Name="ActionPairText"
Margin="0,6,0,0"
FontSize="12.5"
FontWeight="SemiBold"
Foreground="{DynamicResource Brush.Accent}"
Text="" />
<TextBlock
Margin="0,6,0,0"
Style="{StaticResource WindowHintText}"
Text="本地和远端同时修改了同一笔记,请选择保留哪个版本,或手动合并两者内容。" />
Text="请先查看差异,再选择保留本地、保留远端,或手动合并。不会自动合并正文。" />

<StackPanel Margin="0,12,0,0" Orientation="Horizontal">
<RadioButton
x:Name="ViewSideBySide"
Margin="0,0,16,0"
Checked="OnViewModeChanged"
Content="对照"
GroupName="ConflictView" />
<RadioButton
x:Name="ViewDiff"
Margin="0,0,16,0"
Checked="OnViewModeChanged"
Content="行级 Diff"
GroupName="ConflictView" />
<RadioButton
x:Name="ViewPreview"
Checked="OnViewModeChanged"
Content="可读预览"
GroupName="ConflictView" />
</StackPanel>
</StackPanel>
</Border>

Expand All @@ -43,10 +110,12 @@

<StackPanel Margin="-8,0,0,0" HorizontalAlignment="Right" Orientation="Horizontal">
<Button
x:Name="KeepLocalButton"
Click="OnKeepLocal"
Content="保留本地版本"
Style="{StaticResource DialogButtonSuccess}" />
<Button
x:Name="KeepRemoteButton"
Click="OnKeepRemote"
Content="保留远端版本"
Style="{StaticResource DialogButtonPrimary}" />
Expand All @@ -58,6 +127,10 @@
Click="OnKeepAllLocal"
Content="全部保留本地"
Style="{StaticResource DialogButtonMuted}" />
<Button
Click="OnKeepAllRemote"
Content="全部保留远端"
Style="{StaticResource DialogButtonMuted}" />
<Button
Click="OnClose"
Content="关闭"
Expand All @@ -66,7 +139,8 @@
</DockPanel>
</Border>

<Grid Margin="16">
<!-- 对照 -->
<Grid x:Name="SideBySidePanel" Margin="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
Expand Down Expand Up @@ -129,5 +203,83 @@
</Border>
</DockPanel>
</Grid>

<!-- Diff -->
<Border
x:Name="DiffPanel"
Margin="16"
BorderBrush="{DynamicResource Brush.Border}"
BorderThickness="1"
CornerRadius="6"
Style="{StaticResource WindowContentCard}"
Visibility="Collapsed">
<DockPanel>
<Border
Padding="12,8"
Background="{DynamicResource Brush.SurfaceAlt}"
BorderBrush="{DynamicResource Brush.BorderLight}"
BorderThickness="0,0,0,1"
DockPanel.Dock="Top">
<TextBlock
FontSize="12"
Foreground="{DynamicResource Brush.TextSecondary}"
Text="红色 − 仅本地 · 绿色 + 仅远端 · 以本地为基准对比远端" />
</Border>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="DiffLinesList" ItemTemplate="{StaticResource DiffLineTemplate}" />
</ScrollViewer>
</DockPanel>
</Border>

<!-- 可读预览 -->
<Grid x:Name="PreviewPanel" Margin="16" Visibility="Collapsed">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<DockPanel Grid.Column="0">
<Border
Padding="12,8"
Background="{DynamicResource Brush.Success}"
CornerRadius="6,6,0,0"
DockPanel.Dock="Top">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Foreground="White"
Text="本地预览" />
</Border>
<Border
BorderBrush="{DynamicResource Brush.Border}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,6,6"
Style="{StaticResource WindowContentCard}">
<search:NoteSearchPreviewControl x:Name="LocalPreview" />
</Border>
</DockPanel>

<DockPanel Grid.Column="2">
<Border
Padding="12,8"
Background="{DynamicResource Brush.Accent}"
CornerRadius="6,6,0,0"
DockPanel.Dock="Top">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Foreground="White"
Text="远端预览" />
</Border>
<Border
BorderBrush="{DynamicResource Brush.Border}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,6,6"
Style="{StaticResource WindowContentCard}">
<search:NoteSearchPreviewControl x:Name="RemotePreview" />
</Border>
</DockPanel>
</Grid>
</DockPanel>
</Window>
Loading
Loading