Skip to content

Commit 5983d0c

Browse files
committed
优化用户体验
本次更新修复了三个问题,已在Windows系统中完成测试。 1. 现在点击右下角的图标可以直接显示便签管理界面。 2. 在设置中,如果没有开启同步,会自动隐藏同步信息填写的表单,开启同步后再展示。 3. 添加最大化窗口的按钮,位置位于关闭按钮的左侧;调整了全屏按钮的位置,位置位于最大化窗口的左侧;现在可以在窗口边缘拖动修改便签窗口大小,也可以使用之前的快速拖动的方式调整窗口大小。
1 parent 7f75a61 commit 5983d0c

8 files changed

Lines changed: 174 additions & 11 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,8 @@ Default to surfacing uncertainty, not hiding it.
128128

129129
- `AGENTS.md` predates the Avalonia/`src/` refactor and still describes a WPF app with a single root `YASN.csproj` and root-level entry points — its **layout, paths, and `dotnet run`/`publish` commands are stale**; its hard rules and style guidance remain authoritative. Trust this file and the README for layout.
130130
- The app is **GUI-subsystem** (`OutputType=WinExe`) yet doubles as a CLI: in tray mode it has no console, so `AppLogger` only echoes to a console under `#if DEBUG` or when diagnose mode raises one (see `Diagnostics/DiagnoseMode.cs`). The CLI path attaches to the parent terminal (`Cli/ConsoleInterop.cs`).
131+
132+
133+
## 验证环节
134+
135+
- 请使用powershell而不是bash。

src/YASN.App/Application/TrayShell.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ public void Initialize()
100100
IsVisible = true,
101101
};
102102

103+
// Left-clicking the tray icon opens the manage-notes window; right-click still shows Menu.
104+
trayIcon.Clicked += (_, _) => OpenMainWindow();
105+
103106
noteWindows.SetOpenMainWindowAction(OpenMainWindow);
104107
tutorial.SeedOnFirstRun();
105108
noteWindows.RestoreOpenNotes();

src/YASN.App/Localization/LocalizationService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public sealed class LocalizationService
2727
["Window.EditorMode.TextOnly"] = "Edit",
2828
["Window.EditorMode.TextAndPreview"] = "Split",
2929
["Window.FullScreen.Hint"] = "Toggle full screen",
30+
["Window.Maximize.Hint"] = "Maximize / restore",
3031
["Window.EditExternal"] = "Edit in external editor",
3132
["Window.QuickLayout"] = "Quick layout",
3233
["Window.QuickLayout.Hint"] = "Click a monitor to move · drag to resize · Esc to cancel",
@@ -205,6 +206,7 @@ public sealed class LocalizationService
205206
["Window.EditorMode.TextOnly"] = "编辑",
206207
["Window.EditorMode.TextAndPreview"] = "双栏",
207208
["Window.FullScreen.Hint"] = "切换全屏",
209+
["Window.Maximize.Hint"] = "最大化 / 还原",
208210
["Window.EditExternal"] = "使用外部编辑器编辑",
209211
["Window.QuickLayout"] = "快速布局",
210212
["Window.QuickLayout.Hint"] = "点击窗口缩略图以移动 · 拖动以缩放 · 按下 Esc 退出",

src/YASN.App/Settings/SettingsSchema.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,25 @@ public bool BoolValue
9696
}
9797
}
9898

99+
private bool _isFieldVisible = true;
100+
101+
/// <summary>
102+
/// Gets or sets whether this field's editor row is shown in the settings window. Defaults to
103+
/// visible. Used to conditionally hide fields that only apply when another field is set (e.g.
104+
/// the sync-detail fields are hidden while sync is disabled). Hiding leaves <see cref="Value"/>
105+
/// and <see cref="BoolValue"/> intact, so the entered content survives and still persists on save.
106+
/// </summary>
107+
public bool IsFieldVisible
108+
{
109+
get => _isFieldVisible;
110+
set
111+
{
112+
if (_isFieldVisible == value) return;
113+
_isFieldVisible = value;
114+
OnPropertyChanged();
115+
}
116+
}
117+
99118
public Action<SettingField>? OnChanged { get; set; }
100119
public ObservableCollection<SettingOption> Options { get; } = new();
101120

@@ -154,11 +173,35 @@ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
154173
}
155174
}
156175

157-
public class SettingAction
176+
public class SettingAction : INotifyPropertyChanged
158177
{
159178
public string? Key { get; set; }
160179
public string? Label { get; set; }
161180
public Func<Task<string>>? ExecuteAsync { get; set; }
181+
182+
private bool _isActionVisible = true;
183+
184+
/// <summary>
185+
/// Gets or sets whether this action's button is shown in the settings window. Defaults to
186+
/// visible. Used to hide actions that only apply when a related field is set (e.g. the sync
187+
/// "test connection" button is hidden while sync is disabled).
188+
/// </summary>
189+
public bool IsActionVisible
190+
{
191+
get => _isActionVisible;
192+
set
193+
{
194+
if (_isActionVisible == value) return;
195+
_isActionVisible = value;
196+
OnPropertyChanged();
197+
}
198+
}
199+
200+
public event PropertyChangedEventHandler? PropertyChanged;
201+
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
202+
{
203+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
204+
}
162205
}
163206

164207
public class SettingModule : INotifyPropertyChanged

src/YASN.App/SettingsUi/SettingsSchemaBuilder.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,14 +331,15 @@ private static SettingModule BuildSyncModule()
331331
Title = LocalizationService.Current["Settings.Sync.Module"]
332332
};
333333

334-
module.Fields.Add(new SettingField
334+
SettingField enabled = new SettingField
335335
{
336336
Key = SyncSettings.EnabledKey,
337337
Title = LocalizationService.Current["Settings.Sync.Enabled"],
338338
FieldType = SettingFieldType.Toggle,
339339
ShouldSync = false,
340340
BoolValue = false
341-
});
341+
};
342+
module.Fields.Add(enabled);
342343

343344
module.Fields.Add(new SettingField
344345
{
@@ -416,6 +417,30 @@ private static SettingModule BuildSyncModule()
416417
ExecuteAsync = () => TestConnectionAsync(module)
417418
});
418419

420+
// The sync-detail fields (everything except the Enabled toggle) and the test-connection
421+
// action only apply when sync is on. Hide them while sync is off, but keep each field's
422+
// in-memory Value/BoolValue so entered content is preserved and still persists on save.
423+
// Driven live off the Enabled toggle's change hook, with the initial state seeded here
424+
// (ApplyValues re-fires this via BoolValue when a value is loaded).
425+
void SyncDetailVisibility(SettingField toggle)
426+
{
427+
foreach (SettingField field in module.Fields)
428+
{
429+
if (!ReferenceEquals(field, toggle))
430+
{
431+
field.IsFieldVisible = toggle.BoolValue;
432+
}
433+
}
434+
435+
foreach (SettingAction action in module.Actions)
436+
{
437+
action.IsActionVisible = toggle.BoolValue;
438+
}
439+
}
440+
441+
enabled.OnChanged = SyncDetailVisibility;
442+
SyncDetailVisibility(enabled);
443+
419444
return module;
420445
}
421446

src/YASN.App/Views/FloatingNoteWindow.axaml

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,18 @@
3232
<SolidColorBrush x:Key="NotePreviewBackground">#1E1E1E</SolidColorBrush>
3333
</ResourceDictionary>
3434
</ResourceDictionary.ThemeDictionaries>
35+
<!-- Invisible-but-hittable template for the borderless-window edge/corner resize thumbs: a
36+
transparent Border is hit-testable (unlike a null background) yet draws nothing. -->
37+
<ControlTemplate x:Key="InvisibleThumbTemplate" TargetType="Thumb">
38+
<Border Background="Transparent" />
39+
</ControlTemplate>
3540
</ResourceDictionary>
3641
</Window.Resources>
3742
<!-- Keep the title bar and native preview host in separate layout rows. Native WebView hosts can
38-
draw outside Avalonia overlay ordering on macOS, so structural layout is the stable boundary. -->
39-
<Grid RowDefinitions="Auto,*">
43+
draw outside Avalonia overlay ordering on macOS, so structural layout is the stable boundary.
44+
The root grid background fills the resize gutter reserved around the body (see BodyContent's
45+
margin) so the edge/corner thumbs there are never covered by the native WebView. -->
46+
<Grid RowDefinitions="Auto,*" Background="{DynamicResource NoteTitleBarBackground}">
4047
<Border
4148
x:Name="TitleBar"
4249
Grid.Row="0"
@@ -62,22 +69,27 @@
6269
<Button x:Name="EditorModeButton" ToolTip.Tip="{l:Tr Window.EditorMode.Hint}" Click="HandleEditorModeClick">
6370
<mi:MaterialIcon x:Name="EditorModeIcon" Width="18" Height="18" />
6471
</Button>
65-
<Button x:Name="FullScreenButton" ToolTip.Tip="{l:Tr Window.FullScreen.Hint}" Click="HandleFullScreenClick">
66-
<mi:MaterialIcon x:Name="FullScreenIcon" Kind="Fullscreen" Width="18" Height="18" />
67-
</Button>
6872
<Button ToolTip.Tip="{l:Tr Window.QuickLayout.Hint}" Click="HandleQuickLayoutClick">
6973
<mi:MaterialIcon Kind="ViewDashboardOutline" Width="18" Height="18" />
7074
</Button>
7175
<Button ToolTip.Tip="{l:Tr Window.SetReminder}" Click="HandleSetReminderClick">
7276
<mi:MaterialIcon Kind="BellOutline" Width="18" Height="18" />
7377
</Button>
78+
<Button x:Name="FullScreenButton" ToolTip.Tip="{l:Tr Window.FullScreen.Hint}" Click="HandleFullScreenClick">
79+
<mi:MaterialIcon x:Name="FullScreenIcon" Kind="Fullscreen" Width="18" Height="18" />
80+
</Button>
81+
<Button x:Name="MaximizeButton" ToolTip.Tip="{l:Tr Window.Maximize.Hint}" Click="HandleMaximizeClick">
82+
<mi:MaterialIcon x:Name="MaximizeIcon" Kind="WindowMaximize" Width="18" Height="18" />
83+
</Button>
7484
<Button ToolTip.Tip="{l:Tr Window.Close}" Click="HandleCloseClick">
7585
<mi:MaterialIcon Kind="Close" Width="18" Height="18" />
7686
</Button>
7787
</StackPanel>
7888
</Grid>
7989
</Border>
80-
<Grid x:Name="BodyContent" Grid.Row="1" ColumnDefinitions="*,*">
90+
<!-- Left/right/bottom margin reserves a resize gutter the native WebView cannot cover, so the
91+
edge/corner thumbs below stay hittable. No top gutter: the top edge overlaps the title bar. -->
92+
<Grid x:Name="BodyContent" Grid.Row="1" ColumnDefinitions="*,*" Margin="6,0,6,6">
8193
<DockPanel x:Name="EditorPanel" Grid.Column="0">
8294
<StackPanel
8395
x:Name="EditorToolbar"
@@ -161,5 +173,24 @@
161173
</Thumb.Template>
162174
</Thumb>
163175
</Grid>
176+
<!-- Borderless-window edge/corner resize handles. Thin transparent thumbs overlaid on the window
177+
edges each drive the native resize loop for one WindowEdge, giving side and corner resizing that
178+
a WindowDecorations="None" window has no OS chrome for. Corners are declared after edges so they
179+
win hit-testing where they overlap. The bottom-right corner is served by the visible ResizeGrip
180+
above, so it is omitted here. -->
181+
<Thumb x:Name="ResizeLeft" Grid.Row="0" Grid.RowSpan="2" Template="{StaticResource InvisibleThumbTemplate}"
182+
Width="6" HorizontalAlignment="Left" VerticalAlignment="Stretch" Margin="0,12" Cursor="LeftSide" />
183+
<Thumb x:Name="ResizeRight" Grid.Row="0" Grid.RowSpan="2" Template="{StaticResource InvisibleThumbTemplate}"
184+
Width="6" HorizontalAlignment="Right" VerticalAlignment="Stretch" Margin="0,12" Cursor="RightSide" />
185+
<Thumb x:Name="ResizeTop" Grid.Row="0" Grid.RowSpan="2" Template="{StaticResource InvisibleThumbTemplate}"
186+
Height="6" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="12,0" Cursor="TopSide" />
187+
<Thumb x:Name="ResizeBottom" Grid.Row="0" Grid.RowSpan="2" Template="{StaticResource InvisibleThumbTemplate}"
188+
Height="6" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Margin="12,0" Cursor="BottomSide" />
189+
<Thumb x:Name="ResizeTopLeft" Grid.Row="0" Grid.RowSpan="2" Template="{StaticResource InvisibleThumbTemplate}"
190+
Width="12" Height="12" HorizontalAlignment="Left" VerticalAlignment="Top" Cursor="TopLeftCorner" />
191+
<Thumb x:Name="ResizeTopRight" Grid.Row="0" Grid.RowSpan="2" Template="{StaticResource InvisibleThumbTemplate}"
192+
Width="12" Height="12" HorizontalAlignment="Right" VerticalAlignment="Top" Cursor="TopRightCorner" />
193+
<Thumb x:Name="ResizeBottomLeft" Grid.Row="0" Grid.RowSpan="2" Template="{StaticResource InvisibleThumbTemplate}"
194+
Width="12" Height="12" HorizontalAlignment="Left" VerticalAlignment="Bottom" Cursor="BottomLeftCorner" />
164195
</Grid>
165196
</Window>

src/YASN.App/Views/FloatingNoteWindow.axaml.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public sealed partial class FloatingNoteWindow : Window, ILiveNoteContentEditor
4343
private readonly Button editorModeButton;
4444
private readonly Material.Icons.Avalonia.MaterialIcon editorModeIcon;
4545
private readonly Material.Icons.Avalonia.MaterialIcon fullScreenIcon;
46+
private readonly Material.Icons.Avalonia.MaterialIcon maximizeIcon;
4647
private readonly EditorHotkeyController editorHotkeys;
4748
private CompletionWindow? completionWindow;
4849
private List<WindowLevel> supportedLevels = new();
@@ -106,6 +107,8 @@ public FloatingNoteWindow(
106107
?? throw new InvalidOperationException("EditorModeIcon was not found.");
107108
fullScreenIcon = this.FindControl<Material.Icons.Avalonia.MaterialIcon>("FullScreenIcon")
108109
?? throw new InvalidOperationException("FullScreenIcon was not found.");
110+
maximizeIcon = this.FindControl<Material.Icons.Avalonia.MaterialIcon>("MaximizeIcon")
111+
?? throw new InvalidOperationException("MaximizeIcon was not found.");
109112

110113
Grid contentGrid = (Grid)editorPanel.Parent!;
111114
editorColumn = contentGrid.ColumnDefinitions[0];
@@ -115,6 +118,7 @@ public FloatingNoteWindow(
115118
previewWebView.EnvironmentRequested += HandlePreviewEnvironmentRequested;
116119
previewWebView.NavigationCompleted += (_, _) => ScrollPreviewToCaretLine(onlyIfOffscreen: false, smooth: false);
117120
resizeGrip.AddHandler(Thumb.PointerPressedEvent, HandleResizeGripPressed, RoutingStrategies.Tunnel);
121+
RegisterEdgeResizeThumbs();
118122

119123
// Debounce caret moves: editing/cursor changes fire rapidly, but each preview scroll is a
120124
// WebView script call. Coalesce to the latest line on a short timer before invoking JS.
@@ -662,6 +666,18 @@ private void HandleFullScreenClick(object? sender, RoutedEventArgs e)
662666
fullScreenIcon.Kind = entering ? Material.Icons.MaterialIconKind.FullscreenExit : Material.Icons.MaterialIconKind.Fullscreen;
663667
}
664668

669+
/// <summary>
670+
/// Toggles the window between maximized and normal, filling the screen's work area (task bar
671+
/// preserved). Like full-screen this is transient state, not persisted; the icon is swapped so
672+
/// the button reads as "maximize" vs "restore".
673+
/// </summary>
674+
private void HandleMaximizeClick(object? sender, RoutedEventArgs e)
675+
{
676+
bool maximizing = WindowState != WindowState.Maximized;
677+
WindowState = maximizing ? WindowState.Maximized : WindowState.Normal;
678+
maximizeIcon.Kind = maximizing ? Material.Icons.MaterialIconKind.WindowRestore : Material.Icons.MaterialIconKind.WindowMaximize;
679+
}
680+
665681
/// <summary>
666682
/// Opens the note's Markdown file in the operating system's default handler for editing
667683
/// outside the app. Image and relative-link resolution in the external editor is out of scope.
@@ -1257,6 +1273,43 @@ private void HandleResizeGripPressed(object? sender, PointerPressedEventArgs e)
12571273
}
12581274
}
12591275

1276+
/// <summary>
1277+
/// Binds each borderless-window edge/corner resize thumb to the native resize loop for its edge.
1278+
/// The window has no OS chrome (<c>WindowDecorations="None"</c>), so these transparent handles are
1279+
/// the only way to resize from a side or a corner other than the visible bottom-right grip.
1280+
/// </summary>
1281+
private void RegisterEdgeResizeThumbs()
1282+
{
1283+
(string Name, WindowEdge Edge)[] handles =
1284+
{
1285+
("ResizeLeft", WindowEdge.West),
1286+
("ResizeRight", WindowEdge.East),
1287+
("ResizeTop", WindowEdge.North),
1288+
("ResizeBottom", WindowEdge.South),
1289+
("ResizeTopLeft", WindowEdge.NorthWest),
1290+
("ResizeTopRight", WindowEdge.NorthEast),
1291+
("ResizeBottomLeft", WindowEdge.SouthWest)
1292+
};
1293+
1294+
foreach ((string name, WindowEdge edge) in handles)
1295+
{
1296+
Thumb thumb = this.FindControl<Thumb>(name)
1297+
?? throw new InvalidOperationException($"{name} was not found.");
1298+
// Tunnel like the corner grip so the press starts the resize before the thumb's own
1299+
// drag handling consumes it. Capture the edge per-thumb via the closure.
1300+
thumb.AddHandler(
1301+
Thumb.PointerPressedEvent,
1302+
(_, e) =>
1303+
{
1304+
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
1305+
{
1306+
BeginResizeDrag(edge, e);
1307+
}
1308+
},
1309+
RoutingStrategies.Tunnel);
1310+
}
1311+
}
1312+
12601313
private void PersistCurrentBounds()
12611314
{
12621315
AppLogger.Debug($"Note '{viewModel.NoteId}' bounds: pos=({Position.X},{Position.Y}) sizeDip={Width}x{Height} mode={viewModel.DisplayMode}");

src/YASN.App/Views/SettingsWindow.axaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<ItemsControl ItemsSource="{Binding Fields}">
2828
<ItemsControl.ItemTemplate>
2929
<DataTemplate x:DataType="s:SettingField">
30-
<StackPanel Margin="0,4" Spacing="2">
30+
<StackPanel Margin="0,4" Spacing="2" IsVisible="{Binding IsFieldVisible}">
3131
<TextBlock Text="{Binding Title}" FontWeight="Medium" TextWrapping="Wrap" />
3232
<StackPanel Orientation="Horizontal" Spacing="16"
3333
IsVisible="{Binding FieldType, Converter={StaticResource FieldTypeVisibility}, ConverterParameter=Toggle}">
@@ -80,7 +80,8 @@
8080
<ItemsControl.ItemTemplate>
8181
<DataTemplate x:DataType="s:SettingAction">
8282
<Button Content="{Binding Label}" HorizontalAlignment="Left" Margin="0,6,0,0"
83-
Tag="{Binding}" Click="HandleActionClick" />
83+
Tag="{Binding}" Click="HandleActionClick"
84+
IsVisible="{Binding IsActionVisible}" />
8485
</DataTemplate>
8586
</ItemsControl.ItemTemplate>
8687
</ItemsControl>

0 commit comments

Comments
 (0)