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
3 changes: 2 additions & 1 deletion src/BiSheng.Latte/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ private void OnToolbarDrag(object sender, MouseButtonEventArgs e)
{
_vm.UpdateConnectionStatus();
await _vm.RestartSyncEngineAsync();
})
},
getPendingChangeCount: () => _vm.GetPendingChangeCount())
{ Owner = this }.ShowDialog();
}

Expand Down
57 changes: 40 additions & 17 deletions src/BiSheng.Latte/Models/ConnectionDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public enum ConnectionDisplayState
/// <summary>存在未解决冲突</summary>
Conflict,

/// <summary>已验证且处于空闲/实时连接</summary>
/// <summary>已连通,但本地仍有待推送变更</summary>
PendingPush,

/// <summary>已验证且处于空闲/实时连接,本地无待推送</summary>
Synced,
}

Expand Down Expand Up @@ -79,12 +82,14 @@ public static class ConnectionDisplayResolver
/// <param name="hasConflicts">是否有未解决冲突</param>
/// <param name="conflictCount">冲突数量</param>
/// <param name="activityMessage">引擎最近活动文案(推送中/错误详情等,可空)</param>
/// <param name="pendingChangeCount">本地待推送变更条数</param>
public static ConnectionDisplayInfo Resolve(
AuthService auth,
SyncStatus syncStatus,
bool hasConflicts,
int conflictCount = 0,
string? activityMessage = null)
string? activityMessage = null,
int pendingChangeCount = 0)
{
if (hasConflicts && conflictCount > 0)
{
Expand Down Expand Up @@ -152,20 +157,7 @@ public static ConnectionDisplayInfo Resolve(
auth);
}

// 最近一次 Push/Pull 已成功 → 视为已连通,勿被陈旧的 IsServerVerified=false 盖住
if (syncStatus == SyncStatus.Connected || auth.IsServerVerified == true)
{
return Create(
ConnectionDisplayState.Synced,
"已同步",
PreferActivity(activityMessage, "已同步"),
"已与服务器连接,变更会自动同步。",
ThemeBrushKeys.Success,
"\uE73E",
auth);
}

if (auth.IsServerVerified == false)
if (auth.IsServerVerified == false && syncStatus != SyncStatus.Connected)
{
return Create(
ConnectionDisplayState.CannotConnect,
Expand All @@ -179,16 +171,47 @@ public static ConnectionDisplayInfo Resolve(

if (syncStatus == SyncStatus.Error)
{
var errorDetail = pendingChangeCount > 0
? $"最近一次同步失败,本地仍有 {pendingChangeCount} 条未推送,将在网络恢复或下次轮询时重试。"
: "最近一次同步失败,将在网络恢复或下次轮询时重试。";
return Create(
ConnectionDisplayState.SyncError,
"待重试",
PreferActivity(activityMessage, "同步失败,待重试"),
"最近一次同步失败,将在网络恢复或下次轮询时重试。",
errorDetail,
ThemeBrushKeys.Danger,
"\uE946",
auth);
}

// 最近一次 Push/Pull 已成功,或已验证连通 → 勿被陈旧的 IsServerVerified=false 盖住
if (syncStatus == SyncStatus.Connected || auth.IsServerVerified == true)
{
if (pendingChangeCount > 0)
{
var shortLabel = pendingChangeCount > 1
? $"有未推送 ({pendingChangeCount})"
: "有未推送";
return Create(
ConnectionDisplayState.PendingPush,
shortLabel,
PreferActivity(activityMessage, $"有 {pendingChangeCount} 条未推送"),
$"本地还有 {pendingChangeCount} 条变更尚未推送到服务器,将自动重试。当前不代表已全部上云。",
ThemeBrushKeys.Accent,
"\uE898",
auth);
}

return Create(
ConnectionDisplayState.Synced,
"已同步",
PreferActivity(activityMessage, "已同步"),
"已与服务器连接,本地无待推送变更。",
ThemeBrushKeys.Success,
"\uE73E",
auth);
}

return Create(
ConnectionDisplayState.PendingVerify,
"待验证",
Expand Down
7 changes: 7 additions & 0 deletions src/BiSheng.Latte/Services/LocalChangeTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ public void ApplyPendingChange(
_editJournal?.Append(db, entityType, entityId, action, journalTitle, journalContent);
}

/// <summary>当前待推送变更条数(供连接状态徽章展示)</summary>
public int GetPendingChangeCount()
{
using var db = _dbFactory();
return db.PendingChanges.Count();
}

/// <summary>通知同步引擎有新的本地变更(与 ApplyPendingChange 分离,便于合并事务后只触发一次)</summary>
public void NotifyChangeRecorded() => OnChangeRecorded?.Invoke();
}
26 changes: 25 additions & 1 deletion src/BiSheng.Latte/ViewModels/MainViewModel.Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,39 @@ private void ApplyNavColumnWidth(double value)
/// <summary>更新连接状态展示</summary>
public void UpdateConnectionStatus() => RefreshConnectionDisplay();

/// <summary>当前本地待推送变更条数</summary>
public int GetPendingChangeCount()
{
try
{
return _changeTracker.GetPendingChangeCount();
}
catch
{
return 0;
}
}

/// <summary>根据认证与同步状态刷新工具栏徽章与底栏文案</summary>
public void RefreshConnectionDisplay()
{
var pendingCount = 0;
try
{
pendingCount = _changeTracker.GetPendingChangeCount();
}
catch (Exception ex)
{
LogHelper.Debug("读取待推送数量失败: {0}", ex.Message);
}

ConnectionDisplay = ConnectionDisplayResolver.Resolve(
AuthService,
_currentSyncStatus,
HasConflicts,
ConflictCount,
_lastSyncActivityMessage);
_lastSyncActivityMessage,
pendingCount);
SyncStatusText = ConnectionDisplay.StatusBarText;
}

Expand Down
12 changes: 10 additions & 2 deletions src/BiSheng.Latte/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public MainViewModel(
_ = navigationFilterBridge;
_navigationLayoutMode.IsTreeMode = IsTreeMode;

_changeTracker.OnChangeRecorded += ScheduleDebouncedPush;
_changeTracker.OnChangeRecorded += OnLocalChangeRecorded;

FolderTree.SetNoteList(NoteList);
FolderTree.SetNavigation(Navigation);
Expand Down Expand Up @@ -450,6 +450,14 @@ private void ToggleFolderPanel()
IsFolderPanelVisible = !IsFolderPanelVisible;
}

private void OnLocalChangeRecorded()
{
System.Windows.Application.Current?.Dispatcher.BeginInvoke(
DispatcherPriority.Background,
RefreshConnectionDisplay);
ScheduleDebouncedPush();
}

private void ScheduleDebouncedPush()
{
if (Volatile.Read(ref _disposeState) != 0 || !AuthService.IsConnected)
Expand Down Expand Up @@ -485,7 +493,7 @@ public void Dispose()
return;
}

_changeTracker.OnChangeRecorded -= ScheduleDebouncedPush;
_changeTracker.OnChangeRecorded -= OnLocalChangeRecorded;
AuthService.PropertyChanged -= OnAuthServicePropertyChanged;

lock (_debouncedPushLock)
Expand Down
21 changes: 19 additions & 2 deletions src/BiSheng.Latte/Views/SyncSettingsWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public partial class SyncSettingsWindow : Window
private readonly AuthService _authService;
private readonly Func<Task>? _onSyncEnabledChanged;
private readonly Action? _refreshConnectionStatus;
private readonly Func<int>? _getPendingChangeCount;
private readonly Func<Task>? _onDismissWithoutSaveAfterToggle;
private readonly string _savedServerUrl;
private readonly string _savedApiKey;
Expand All @@ -27,11 +28,13 @@ public SyncSettingsWindow(
Func<Task>? onSyncEnabledChanged = null,
Action? refreshConnectionStatus = null,
int initialTabIndex = 0,
Func<Task>? onDismissWithoutSaveAfterToggle = null)
Func<Task>? onDismissWithoutSaveAfterToggle = null,
Func<int>? getPendingChangeCount = null)
{
_authService = authService;
_onSyncEnabledChanged = onSyncEnabledChanged;
_refreshConnectionStatus = refreshConnectionStatus;
_getPendingChangeCount = getPendingChangeCount;
_onDismissWithoutSaveAfterToggle = onDismissWithoutSaveAfterToggle;
_savedServerUrl = authService.ServerUrl ?? string.Empty;
_savedApiKey = authService.ApiKey ?? string.Empty;
Expand Down Expand Up @@ -60,7 +63,21 @@ private void LoadConnectionFields()

private void UpdateConnectionStatusDisplay()
{
var display = ConnectionDisplayResolver.Resolve(_authService, SyncStatus.Idle, hasConflicts: false);
var pending = 0;
try
{
pending = _getPendingChangeCount?.Invoke() ?? 0;
}
catch
{
/* 读取失败时按无待推送展示 */
}

var display = ConnectionDisplayResolver.Resolve(
_authService,
SyncStatus.Idle,
hasConflicts: false,
pendingChangeCount: pending);
ConnectionStatusText.Text = display.DetailText;
ConnectionStatusText.Foreground = TryFindResource(display.BrushKey) as Brush
?? TryFindResource(ThemeBrushKeys.TextMuted) as Brush
Expand Down
8 changes: 8 additions & 0 deletions src/docs/同步与安全设置使用指南.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

对话框含三个页签:**连接** / **同步** / **数据安全**。顶部会显示当前连接状态。

主窗口工具栏 / 底栏连接徽章含义(摘要):

| 展示 | 含义 |
|------|------|
| **已同步** | 已连通,且本地**无**待推送变更 |
| **有未推送 (N)** | 已连通,但本地仍有 N 条变更等待上传(不等于已全部上云) |
| **同步中** / **冲突** / **待重试** 等 | 正在推拉、有冲突或同步失败 |

---

## 快速上手:三种常见场景
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using BiSheng.Latte.Models;
using BiSheng.Latte.Services;

namespace BiSheng.Latte.Tests.Services;

/// <summary>连接徽章:有待推送时不得显示「已同步」</summary>
public class ConnectionDisplayResolverTests
{
private static AuthService CreateVerifiedAuth()
{
var auth = new AuthService
{
ServerUrl = "http://127.0.0.1:8090",
ApiKey = "test-key",
IsSyncEnabled = true,
Username = "u"
};
auth.SetServerVerified(true);
return auth;
}

[Fact]
public void Resolve_WhenConnectedWithPending_ShowsPendingPushNotSynced()
{
var display = ConnectionDisplayResolver.Resolve(
CreateVerifiedAuth(),
SyncStatus.Connected,
hasConflicts: false,
pendingChangeCount: 3);

Assert.Equal(ConnectionDisplayState.PendingPush, display.State);
Assert.Equal("有未推送 (3)", display.ShortLabel);
Assert.Contains("3", display.StatusBarText);
Assert.Contains("尚未推送", display.DetailText);
Assert.DoesNotContain("已同步", display.ShortLabel);
Assert.Equal(ThemeBrushKeys.Accent, display.BrushKey);
}

[Fact]
public void Resolve_WhenConnectedWithoutPending_ShowsSynced()
{
var display = ConnectionDisplayResolver.Resolve(
CreateVerifiedAuth(),
SyncStatus.Connected,
hasConflicts: false,
pendingChangeCount: 0);

Assert.Equal(ConnectionDisplayState.Synced, display.State);
Assert.Equal("已同步", display.ShortLabel);
Assert.Contains("无待推送", display.DetailText);
Assert.Equal(ThemeBrushKeys.Success, display.BrushKey);
}

[Fact]
public void Resolve_WhenPushingWithPending_StillShowsSyncing()
{
var display = ConnectionDisplayResolver.Resolve(
CreateVerifiedAuth(),
SyncStatus.Pushing,
hasConflicts: false,
pendingChangeCount: 2);

Assert.Equal(ConnectionDisplayState.Syncing, display.State);
Assert.Equal("同步中", display.ShortLabel);
}

[Fact]
public void Resolve_ConflictOverridesPending()
{
var display = ConnectionDisplayResolver.Resolve(
CreateVerifiedAuth(),
SyncStatus.Connected,
hasConflicts: true,
conflictCount: 1,
pendingChangeCount: 5);

Assert.Equal(ConnectionDisplayState.Conflict, display.State);
Assert.True(display.OpensConflicts);
}
}
Loading