Compare commits

...

5 Commits

Author SHA1 Message Date
lincube
685323e057 0.7.5
顺滑的组件放置与调整
2026-03-22 15:21:29 +08:00
lincube
def21c79b1 0.7.4.1
动画优化
2026-03-22 14:47:15 +08:00
lincube
c3db5af923 0.7.4
首先我加了CI课程表json的读取,然后把天气时钟这个老问题也修了。
2026-03-22 04:57:19 +08:00
lincube
1a7dde34d0 0.7.3.1 2026-03-22 02:53:31 +08:00
lincube
73cdefe296 0.7.3
修东西
2026-03-21 22:40:07 +08:00
55 changed files with 5332 additions and 647 deletions

View File

@@ -0,0 +1,60 @@
using Avalonia;
using LanMountainDesktop.DesktopEditing;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class ComponentLibraryCollapseStateTests
{
[Fact]
public void CreateExpanded_InitializesExpandedStateAndHidesChip()
{
var margin = new Thickness(24, 24, 24, 100);
var state = ComponentLibraryCollapseState.CreateExpanded(margin, 0.75);
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, state.VisualState);
Assert.Equal(margin, state.ExpandedMargin);
Assert.Equal(0.75, state.ExpandedOpacity, 3);
Assert.False(state.IsChipVisible);
}
[Fact]
public void WithVisualState_PreservesStableExpandedSnapshotAcrossTransitions()
{
var margin = new Thickness(20, 18, 20, 96);
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 1);
var collapsing = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Collapsing, isChipVisible: true);
var collapsed = collapsing.WithVisualState(ComponentLibraryCollapseVisualState.Collapsed, isChipVisible: true);
var restoring = collapsed.WithVisualState(ComponentLibraryCollapseVisualState.Restoring, isChipVisible: false);
Assert.Equal(ComponentLibraryCollapseVisualState.Collapsing, collapsing.VisualState);
Assert.Equal(ComponentLibraryCollapseVisualState.Collapsed, collapsed.VisualState);
Assert.Equal(ComponentLibraryCollapseVisualState.Restoring, restoring.VisualState);
Assert.Equal(margin, collapsing.ExpandedMargin);
Assert.Equal(margin, collapsed.ExpandedMargin);
Assert.Equal(margin, restoring.ExpandedMargin);
Assert.Equal(1, collapsing.ExpandedOpacity, 3);
Assert.Equal(1, collapsed.ExpandedOpacity, 3);
Assert.Equal(1, restoring.ExpandedOpacity, 3);
Assert.True(collapsing.IsChipVisible);
Assert.True(collapsed.IsChipVisible);
Assert.False(restoring.IsChipVisible);
}
[Fact]
public void CreateExpanded_ProducesRestorableSnapshotEvenWhenOriginalOpacityIsLow()
{
var margin = new Thickness(18, 22, 18, 88);
var expanded = ComponentLibraryCollapseState.CreateExpanded(margin, 0.15);
var restored = expanded.WithVisualState(ComponentLibraryCollapseVisualState.Expanded, isChipVisible: false);
Assert.Equal(margin, restored.ExpandedMargin);
Assert.Equal(0.15, restored.ExpandedOpacity, 3);
Assert.Equal(ComponentLibraryCollapseVisualState.Expanded, restored.VisualState);
Assert.False(restored.IsChipVisible);
}
}

View File

@@ -0,0 +1,15 @@
using LanMountainDesktop.DesktopEditing;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class DesktopEditCommitMathTests
{
[Fact]
public void IsPendingCommitValid_ReturnsTrueOnlyForMatchingActiveVersion()
{
Assert.True(DesktopEditCommitMath.IsPendingCommitValid(isPending: true, scheduledVersion: 4, currentVersion: 4));
Assert.False(DesktopEditCommitMath.IsPendingCommitValid(isPending: false, scheduledVersion: 4, currentVersion: 4));
Assert.False(DesktopEditCommitMath.IsPendingCommitValid(isPending: true, scheduledVersion: 4, currentVersion: 5));
}
}

View File

@@ -0,0 +1,173 @@
using Avalonia;
using LanMountainDesktop.DesktopEditing;
using Xunit;
namespace LanMountainDesktop.Tests;
public sealed class DesktopPlacementMathTests
{
[Fact]
public void ComputeDragStartThreshold_UsesFloorAndCellScale()
{
Assert.Equal(10d, DesktopPlacementMath.ComputeDragStartThreshold(24));
Assert.Equal(14.4d, DesktopPlacementMath.ComputeDragStartThreshold(80), 3);
}
[Fact]
public void HasExceededThreshold_OnlyReturnsTrueAfterEnoughMovement()
{
var start = new Point(20, 20);
Assert.False(DesktopPlacementMath.HasExceededThreshold(start, new Point(27, 25), 10));
Assert.True(DesktopPlacementMath.HasExceededThreshold(start, new Point(31, 20), 10));
}
[Fact]
public void OcclusionHelpers_DetectPointAndRectOverlap()
{
var libraryBounds = new Rect(100, 100, 200, 160);
Assert.True(DesktopPlacementMath.IsOccludedByComponentLibrary(new Point(120, 150), libraryBounds));
Assert.False(DesktopPlacementMath.IsOccludedByComponentLibrary(new Point(80, 90), libraryBounds));
Assert.True(DesktopPlacementMath.IsOccludedByComponentLibrary(new Rect(250, 120, 120, 80), libraryBounds));
Assert.False(DesktopPlacementMath.IsOccludedByComponentLibrary(new Rect(10, 10, 40, 40), libraryBounds));
}
[Fact]
public void TryGetSnappedCell_ClampsInsideGridBounds()
{
var grid = new DesktopGridGeometry(
Origin: default,
CellSize: 80,
CellGap: 8,
ColumnCount: 4,
RowCount: 5);
var result = DesktopPlacementMath.TryGetSnappedCell(
grid,
pointerInViewport: new Point(490, 520),
pointerOffset: new Point(10, 10),
widthCells: 2,
heightCells: 3,
out var column,
out var row);
Assert.True(result);
Assert.Equal(2, column);
Assert.Equal(2, row);
}
[Fact]
public void GetCellRect_MapsCellsToPixelRect()
{
var grid = new DesktopGridGeometry(
Origin: new Point(12, 24),
CellSize: 80,
CellGap: 8,
ColumnCount: 6,
RowCount: 8);
var rect = DesktopPlacementMath.GetCellRect(grid, column: 2, row: 3, widthCells: 2, heightCells: 3);
Assert.Equal(188, rect.X, 3);
Assert.Equal(288, rect.Y, 3);
Assert.Equal(168, rect.Width, 3);
Assert.Equal(256, rect.Height, 3);
}
[Fact]
public void Session_DoesNotCommitWhilePointerIsStillInsideLibrary()
{
var session = DesktopEditSession.CreatePendingNew(
componentId: "demo",
pageIndex: 0,
widthCells: 2,
heightCells: 2,
startPointerInViewport: new Point(80, 80),
pointerOffsetInViewport: new Point(60, 60),
componentLibraryBounds: new Rect(0, 0, 220, 300));
session = session.WithCurrentPointer(new Point(130, 150));
Assert.True(session.HasExceededThreshold(DesktopPlacementMath.ComputeDragStartThreshold(80)));
Assert.True(session.IsPointerInsideComponentLibrary());
Assert.False(session.CanCommit);
}
[Fact]
public void Session_ResizePreviewStillBlocksWhenPointerRemainsInsideLibrary()
{
var session = DesktopEditSession.CreateResizingExisting(
componentId: "demo",
placementId: "placement-1",
pageIndex: 0,
widthCells: 2,
heightCells: 2,
startPointerInViewport: new Point(80, 80),
componentLibraryBounds: new Rect(0, 0, 220, 300))
.WithCurrentPointer(new Point(130, 150));
Assert.True(session.IsPointerInsideComponentLibrary());
Assert.False(session.CanCommit);
}
[Fact]
public void HasCellPositionChanged_DetectsNoOpAndRealMoves()
{
Assert.False(DesktopPlacementMath.HasCellPositionChanged(2, 3, 2, 3));
Assert.True(DesktopPlacementMath.HasCellPositionChanged(2, 3, 2, 4));
}
[Fact]
public void HasCellSpanChanged_DetectsNoOpAndRealResizes()
{
Assert.False(DesktopPlacementMath.HasCellSpanChanged(2, 3, 2, 3));
Assert.True(DesktopPlacementMath.HasCellSpanChanged(2, 3, 3, 3));
}
[Fact]
public void CanCommitPlacement_BlocksWhenPlacementIsOccludedByLibrary()
{
var placementRect = new Rect(160, 110, 180, 140);
var occludingLibraryBounds = new Rect(120, 80, 240, 220);
var distantLibraryBounds = new Rect(420, 420, 80, 80);
Assert.False(DesktopPlacementMath.CanCommitPlacement(placementRect, occludingLibraryBounds));
Assert.True(DesktopPlacementMath.CanCommitPlacement(placementRect, distantLibraryBounds));
Assert.True(DesktopPlacementMath.CanCommitPlacement(placementRect, componentLibraryBounds: null));
}
[Fact]
public void Session_AllowsCommitWhenComponentLibraryBoundsAreCleared()
{
var pendingSession = DesktopEditSession.CreatePendingNew(
componentId: "demo",
pageIndex: 0,
widthCells: 2,
heightCells: 2,
startPointerInViewport: new Point(80, 80),
pointerOffsetInViewport: new Point(60, 60),
componentLibraryBounds: null)
.WithCurrentPointer(new Point(200, 180));
Assert.True(pendingSession.HasExceededThreshold(DesktopPlacementMath.ComputeDragStartThreshold(80)));
Assert.False(pendingSession.IsPointerInsideComponentLibrary());
Assert.False(pendingSession.IsPreviewOccludedByComponentLibrary(new Rect(100, 100, 40, 40)));
Assert.False(pendingSession.CanCommit);
var resizeSession = DesktopEditSession.CreateResizingExisting(
componentId: "demo",
placementId: "placement-1",
pageIndex: 0,
widthCells: 2,
heightCells: 2,
startPointerInViewport: new Point(80, 80),
componentLibraryBounds: null)
.WithCurrentPointer(new Point(200, 180))
.WithTargetCell(row: 2, column: 3);
Assert.False(resizeSession.IsPointerInsideComponentLibrary());
Assert.False(resizeSession.IsPreviewOccludedByComponentLibrary(new Rect(100, 100, 40, 40)));
Assert.True(resizeSession.CanCommit);
}
}

View File

@@ -101,6 +101,11 @@ public partial class App : Application
public App()
{
if (Design.IsDesignMode)
{
return;
}
_settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
}
@@ -108,9 +113,16 @@ public partial class App : Application
public override void Initialize()
{
AppLogger.Info("App", "Initializing application resources.");
AvaloniaXamlLoader.Load(this);
if (Design.IsDesignMode)
{
ApplyDesignTimeTheme();
return;
}
ConfigureWebViewUserDataFolder();
AvaloniaWebViewBuilder.Initialize(default);
AvaloniaXamlLoader.Load(this);
ApplyThemeFromSettings();
ApplyCurrentCultureFromSettings();
EnsureSettingsWindowService();
@@ -119,6 +131,12 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
if (Design.IsDesignMode)
{
base.OnFrameworkInitializationCompleted();
return;
}
AppLogger.Info("App", "Framework initialization completed.");
RegisterUiUnhandledExceptionGuard();
LinuxDesktopEntryInstaller.EnsureInstalled();
@@ -127,6 +145,20 @@ public partial class App : Application
base.OnFrameworkInitializationCompleted();
}
private void ApplyDesignTimeTheme()
{
RequestedThemeVariant = ThemeVariant.Light;
try
{
ApplyAdaptiveThemeResources();
}
catch (Exception ex)
{
AppLogger.Warn("Previewer", "Failed to apply adaptive theme resources in design mode.", ex);
}
}
private void InitializeDesktopShell()
{
_desktopShellHost ??= new DesktopShellHost(

View File

@@ -1,54 +1,229 @@
# 隐私与遥测说明
# LanMountainDesktop 遥测隐私政策
LanMountainDesktop 提供两类可选遥测能力:
**生效日期**2026年3月22日\
**最后更新**2026年3月22日
- 崩溃数据上传
- 行为数据分析
***
这两个开关默认关闭。即使两项都关闭,应用仍会在首次启动时向 PostHog 发送一次最小化的启动基线事件,用于统计用户量。
## 引言
## 默认行为
LanMountainDesktop以下简称"本应用")由 灵方软件Lincube以下简称"我们")开发和维护。我们深知用户隐私的重要性,并致力于保护您的个人信息安全。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的数据。
当“崩溃数据上传”和“行为数据分析”都关闭时:
使用本应用即表示您同意本隐私政策的条款。如果您不同意本政策的任何部分,请停止使用本应用。
- 仅首次启动会发送一次 `app_first_launch` 事件
- 该事件只用于统计用户量
- 事件时间由 PostHog 接入侧记录的请求时间和启动时间决定
- 不会主动上传设备型号、操作系统细节、组件操作轨迹等详细信息
***
## 崩溃数据上传
## 1. 数据收集范围
当开启“崩溃数据上传”时,应用会把崩溃与未处理异常发送到 Sentry用于分析稳定性问题。
### 1.1 我们收集的数据
上报内容可能包括
本应用提供两类可选的数据收集功能
- 异常堆栈和错误上下文
- 应用版本与运行环境
- 操作系统信息
- 设备基础信息
- 最近的日志尾部内容
| 数据类型 | 收集方式 | 默认状态 | 用途 |
| ------ | ---- | ---- | -------- |
| 启动基线事件 | 自动收集 | 开启 | 统计用户量 |
| 崩溃数据 | 用户授权 | 关闭 | 分析稳定性问题 |
| 行为数据 | 用户授权 | 关闭 | 分析功能使用情况 |
应用退出或崩溃时,会尽量补充最后一次会话和日志信息,方便定位问题。
### 1.2 启动基线事件
## 行为数据分析
无论您是否开启其他遥测选项,本应用会在首次启动时发送一次最小化的启动基线事件(`app_first_launch`),用于统计活跃用户量。该事件仅包含:
当开启“行为数据分析”时,应用会把关键行为事件发送到 PostHog用于分析功能使用情况和会话路径。
- 匿名安装标识符Install ID
- 应用版本号
- 启动时间戳
上报内容可能包括:
### 1.3 崩溃数据
- 应用启动和退出时间
- 会话开始与结束时间
- 设置页打开、关闭和导航
- 抽屉打开和关闭
- 桌面组件的放置、移动、缩放、删除和编辑入口
当您开启"崩溃数据上传"功能时,我们可能收集以下信息:
这些事件会被转换成 PostHog 可以直接接收和分析的事件格式,方便在 PostHog 中按事件流查看用户行为。桌面端的“回放”能力通过事件时间线重建,而不是浏览器式 Session Replay。
- **异常信息**:异常类型、错误消息、堆栈跟踪
- **应用信息**:应用版本、构建号、运行时环境
- **系统信息**:操作系统版本、系统架构、可用内存
- **设备信息**:设备型号、屏幕分辨率
- **日志信息**:应用崩溃前的最近日志记录(可能包含您在使用过程中产生的操作记录)
## 身份与隐私控制
### 1.4 行为数据
应用会使用随机生成的匿名 install ID 和可刷新 telemetry ID 来区分安装与运行会话。
当您开启"行为数据分析"功能时,我们可能收集以下信息:
- 刷新 telemetry ID 只会影响后续详细遥测
- 关闭开关后,不会继续发送对应类别的详细遥测
- IP 只会通过 Sentry / PostHog 的服务端接入侧自然记录,不会作为自定义字段重复上报
- **会话信息**:应用启动/退出时间、会话持续时间
- **功能使用**:设置页面访问、抽屉操作、组件库操作
- **组件操作**:桌面组件的放置、移动、调整大小、删除操作
- **界面交互**:页面切换、编辑模式进入/退出
***
## 2. 数据使用目的
我们收集的数据将用于以下目的:
### 2.1 启动基线事件
- 统计应用的用户数量和活跃度
- 了解应用的安装分布情况
### 2.2 崩溃数据
- 诊断和修复应用崩溃问题
- 提高应用的稳定性和可靠性
- 识别和解决性能瓶颈
### 2.3 行为数据
- 了解用户如何使用本应用的功能
- 改进用户体验和界面设计
- 指导功能开发和优先级决策
- 分析用户行为模式和趋势
***
## 3. 数据存储与传输
### 3.1 数据传输
您的数据将通过加密连接传输至以下第三方服务:
| 服务提供商 | 服务类型 | 数据内容 | 隐私政策 |
| ------- | ---- | --------- | ----------------------------- |
| PostHog | 产品分析 | 启动事件、行为数据 | <https://posthog.com/privacy> |
| Sentry | 错误监控 | 崩溃数据、异常信息 | <https://sentry.io/privacy/> |
### 3.2 数据存储位置
数据存储于上述第三方服务的服务器,这些服务器可能位于中国境外。我们已与这些服务提供商签订数据处理协议,确保您的数据得到适当保护。
### 3.3 数据保留期限
- **启动基线事件**:保留期限由 PostHog 服务配置决定,通常为 13 个月
- **崩溃数据**:保留期限由 Sentry 服务配置决定,通常为 90 天
- **行为数据**:保留期限由 PostHog 服务配置决定,通常为 13 个月
***
## 4. 用户权利与控制
### 4.1 您的权利
根据适用的数据保护法律,您享有以下权利:
- **知情权**:了解我们收集哪些数据及其用途
- **访问权**:请求获取我们持有的您的个人数据副本
- **更正权**:请求更正不准确或不完整的个人数据
- **删除权**:请求删除您的个人数据
- **撤回同意权**:随时撤回您对数据收集的同意
- **数据可携带权**:以结构化格式接收您的个人数据
### 4.2 如何行使您的权利
您可以通过以下方式行使上述权利:
1. **关闭遥测功能**:在应用设置 > 隐私设置中关闭相应开关
2. **刷新遥测标识**:在应用设置 > 隐私设置中点击"刷新遥测 ID"
3. **联系我们**:通过 GitHub Issues 提交数据相关请求
### 4.3 功能控制
| 功能 | 控制方式 | 效果 |
| ------- | ---- | ----------- |
| 崩溃数据上传 | 设置开关 | 关闭后停止发送崩溃数据 |
| 行为数据分析 | 设置开关 | 关闭后停止发送行为数据 |
| 刷新遥测 ID | 手动触发 | 生成新的匿名标识符 |
***
## 5. 身份标识
### 5.1 匿名标识符
我们使用以下匿名标识符来区分用户和会话:
- **Install ID**:在应用首次安装时随机生成的唯一标识符,用于区分不同的安装实例
- **Telemetry ID**:匿名标识符,用于关联遥测数据
### 5.2 标识符特性
- 这些标识符不包含您的真实身份信息
- 标识符与您的个人身份(如姓名、邮箱、电话)无关联
***
## 6. 数据安全
### 6.1 安全措施
我们采取以下安全措施保护您的数据:
- **传输加密**:所有数据传输均使用 TLS/HTTPS 加密
- **访问控制**:限制对数据的访问权限,仅授权人员可访问
- **匿名化处理**:使用匿名标识符而非个人身份信息
### 6.2 数据泄露响应
如发生数据泄露事件,我们将:
1. 及时评估泄露的影响范围和严重程度
2. 采取必要措施阻止进一步泄露
3. 根据法律要求通知相关监管机构和受影响用户
***
## 7. 第三方服务
### 7.1 PostHog
PostHog 是我们使用的产品分析平台用于收集和分析用户行为数据。PostHog 的隐私政策请参阅:<https://posthog.com/privacy>
### 7.2 Sentry
Sentry 是我们使用的错误监控平台用于收集和分析崩溃数据。Sentry 的隐私政策请参阅:<https://sentry.io/privacy/>
### 7.3 第三方责任
我们仅将上述第三方服务用于本政策所述目的。我们不对这些第三方的隐私实践负责,建议您阅读其隐私政策。
***
## 8. 儿童隐私
本应用不面向 14 周岁以下的儿童。我们不会故意收集儿童的个人信息。如果您是 14 周岁以下儿童的监护人,且发现您的孩子向我们提供了个人信息,请联系我们,我们将采取措施删除相关信息。
***
## 9. 隐私政策更新
我们可能会不时更新本隐私政策。更新后的政策将在本应用内发布,并在政策顶部注明"最后更新"日期。重大变更时,我们将在应用内通过显著方式通知您。
建议您定期查阅本政策,以了解我们如何保护您的信息。继续使用本应用即表示您接受更新后的隐私政策。
***
## 10. 适用法律
本隐私政策的解释和执行适用中华人民共和国法律法规,包括但不限于:
- 《中华人民共和国个人信息保护法》
- 《中华人民共和国数据安全法》
- 《中华人民共和国网络安全法》
- 《信息安全技术 个人信息安全规范》GB/T 35273
***
## 11. 联系我们
如果您对本隐私政策有任何疑问、意见或建议,请通过以下方式联系我们:
- **GitHub 仓库**<https://github.com/wwiinnddyy/LanMountainDesktop>
- **问题反馈**<https://github.com/wwiinnddyy/LanMountainDesktop/issues>
我们将在收到您的请求后 30 日内予以答复。
***
## 12. 条款可分割性
如果本隐私政策的任何条款被有管辖权的法院或监管机构认定为无效或不可执行,该条款应在最小必要范围内进行修改以使其有效和可执行,或如果无法修改,则予以删除。本政策的其余条款将继续有效。
***
**本隐私政策最终解释权归灵方软件Lincube所有。**

View File

@@ -0,0 +1,280 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
namespace LanMountainDesktop.DesktopEditing;
internal sealed class ComponentLibraryCollapsePresenter
{
private static readonly TimeSpan TransitionDuration = TimeSpan.FromMilliseconds(150);
private static readonly Easing TransitionEasing = new CubicEaseOut();
private const double StableOpacityThreshold = 0.01;
private readonly Border _componentLibraryWindow;
private readonly Border _collapsedChipHost;
private readonly TextBlock _collapsedChipTextBlock;
private readonly Control? _collapsedChipIcon;
private readonly TranslateTransform _windowTranslate = new();
private readonly TranslateTransform _chipTranslate = new();
private readonly ScaleTransform _chipScale = new(1, 1);
private ComponentLibraryCollapseState _state;
private int _transitionVersion;
public ComponentLibraryCollapsePresenter(
Border componentLibraryWindow,
Border collapsedChipHost,
TextBlock collapsedChipTextBlock,
Control? collapsedChipIcon = null)
{
_componentLibraryWindow = componentLibraryWindow ?? throw new ArgumentNullException(nameof(componentLibraryWindow));
_collapsedChipHost = collapsedChipHost ?? throw new ArgumentNullException(nameof(collapsedChipHost));
_collapsedChipTextBlock = collapsedChipTextBlock ?? throw new ArgumentNullException(nameof(collapsedChipTextBlock));
_collapsedChipIcon = collapsedChipIcon;
EnsureTransforms();
_state = ComponentLibraryCollapseState.CreateExpanded(
_componentLibraryWindow.Margin,
_componentLibraryWindow.Opacity <= 0 ? 1 : _componentLibraryWindow.Opacity);
ApplyExpandedSnapshot();
_collapsedChipHost.IsVisible = false;
_collapsedChipHost.IsHitTestVisible = false;
_collapsedChipHost.Opacity = 0;
}
public bool IsCollapsed => _state.VisualState is ComponentLibraryCollapseVisualState.Collapsing or ComponentLibraryCollapseVisualState.Collapsed;
public ComponentLibraryCollapseVisualState VisualState => _state.VisualState;
public void SyncExpandedState(Thickness margin, double opacity)
{
var hasStableOpacity = IsStableExpandedOpacity(opacity);
var nextExpandedOpacity = hasStableOpacity ? Math.Clamp(opacity, 0, 1) : _state.ExpandedOpacity;
_state = _state with
{
ExpandedMargin = margin,
ExpandedOpacity = nextExpandedOpacity
};
if (_state.VisualState is ComponentLibraryCollapseVisualState.Expanded or ComponentLibraryCollapseVisualState.Restoring)
{
ApplyExpandedSnapshot(applyOpacity: hasStableOpacity);
}
}
public void Collapse(string title)
{
_collapsedChipTextBlock.Text = string.IsNullOrWhiteSpace(title) ? "Widgets" : title;
if (_state.VisualState is ComponentLibraryCollapseVisualState.Collapsing or ComponentLibraryCollapseVisualState.Collapsed)
{
ShowCollapsedChip(_transitionVersion);
return;
}
var version = ++_transitionVersion;
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Collapsing, isChipVisible: true);
ApplyExpandedSnapshot();
ShowCollapsedChip(version);
SetCollapsedWindowTargets();
DispatcherTimer.RunOnce(
() =>
{
if (version != _transitionVersion)
{
return;
}
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Collapsed, isChipVisible: true);
_componentLibraryWindow.IsVisible = false;
_componentLibraryWindow.IsHitTestVisible = false;
},
TransitionDuration);
}
public void Restore()
{
if (_state.VisualState is ComponentLibraryCollapseVisualState.Expanded)
{
ApplyExpandedSnapshot();
_collapsedChipHost.IsVisible = false;
_collapsedChipHost.IsHitTestVisible = false;
_collapsedChipHost.Opacity = 0;
return;
}
var version = ++_transitionVersion;
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Restoring, isChipVisible: false);
PrepareRestoringWindow();
HideCollapsedChip(version);
Dispatcher.UIThread.Post(
() =>
{
if (version != _transitionVersion)
{
return;
}
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
_windowTranslate.Y = 0;
},
DispatcherPriority.Background);
DispatcherTimer.RunOnce(
() =>
{
if (version != _transitionVersion)
{
return;
}
_state = _state.WithVisualState(ComponentLibraryCollapseVisualState.Expanded, isChipVisible: false);
_componentLibraryWindow.IsVisible = true;
_componentLibraryWindow.IsHitTestVisible = true;
},
TransitionDuration);
}
private void EnsureTransforms()
{
_componentLibraryWindow.RenderTransform = _windowTranslate;
_windowTranslate.Transitions = new Transitions
{
new DoubleTransition
{
Property = TranslateTransform.YProperty,
Duration = TransitionDuration,
Easing = TransitionEasing
}
};
_collapsedChipHost.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
_collapsedChipHost.RenderTransform = new TransformGroup
{
Children =
{
_chipTranslate,
_chipScale
}
};
_chipTranslate.Transitions = new Transitions
{
new DoubleTransition
{
Property = TranslateTransform.YProperty,
Duration = TransitionDuration,
Easing = TransitionEasing
}
};
_chipScale.Transitions = new Transitions
{
new DoubleTransition
{
Property = ScaleTransform.ScaleXProperty,
Duration = TransitionDuration,
Easing = TransitionEasing
},
new DoubleTransition
{
Property = ScaleTransform.ScaleYProperty,
Duration = TransitionDuration,
Easing = TransitionEasing
}
};
}
private void ApplyExpandedSnapshot(bool applyOpacity = true)
{
_componentLibraryWindow.Margin = _state.ExpandedMargin;
if (applyOpacity)
{
_componentLibraryWindow.Opacity = _state.ExpandedOpacity;
}
_componentLibraryWindow.IsVisible = true;
_componentLibraryWindow.IsHitTestVisible = true;
_windowTranslate.Y = 0;
}
private void SetCollapsedWindowTargets()
{
_componentLibraryWindow.Opacity = 0;
_windowTranslate.Y = 28;
}
private void ShowCollapsedChip(int version)
{
_collapsedChipHost.IsVisible = true;
_collapsedChipHost.IsHitTestVisible = false;
_collapsedChipTextBlock.IsVisible = true;
if (_collapsedChipIcon is not null)
{
_collapsedChipIcon.IsVisible = true;
}
_collapsedChipHost.Opacity = 0;
_chipTranslate.Y = 8;
_chipScale.ScaleX = 0.96;
_chipScale.ScaleY = 0.96;
Dispatcher.UIThread.Post(
() =>
{
if (version != _transitionVersion)
{
return;
}
_collapsedChipHost.Opacity = 1;
_chipTranslate.Y = 0;
_chipScale.ScaleX = 1;
_chipScale.ScaleY = 1;
},
DispatcherPriority.Background);
}
private void HideCollapsedChip(int version)
{
_collapsedChipHost.IsVisible = true;
_collapsedChipHost.IsHitTestVisible = false;
_collapsedChipHost.Opacity = 0;
_chipTranslate.Y = 8;
_chipScale.ScaleX = 0.96;
_chipScale.ScaleY = 0.96;
DispatcherTimer.RunOnce(
() =>
{
if (version != _transitionVersion)
{
return;
}
_collapsedChipHost.IsVisible = false;
},
TransitionDuration);
}
private void PrepareRestoringWindow()
{
_componentLibraryWindow.IsVisible = true;
_componentLibraryWindow.IsHitTestVisible = true;
_componentLibraryWindow.Margin = _state.ExpandedMargin;
_componentLibraryWindow.Opacity = 0;
_windowTranslate.Y = 28;
}
private static bool IsStableExpandedOpacity(double opacity)
{
return !double.IsNaN(opacity) &&
!double.IsInfinity(opacity) &&
opacity > StableOpacityThreshold;
}
}

View File

@@ -0,0 +1,36 @@
using Avalonia;
namespace LanMountainDesktop.DesktopEditing;
internal enum ComponentLibraryCollapseVisualState
{
Expanded,
Collapsing,
Collapsed,
Restoring
}
internal readonly record struct ComponentLibraryCollapseState(
ComponentLibraryCollapseVisualState VisualState,
Thickness ExpandedMargin,
double ExpandedOpacity,
bool IsChipVisible)
{
public static ComponentLibraryCollapseState CreateExpanded(Thickness expandedMargin, double expandedOpacity)
{
return new(
ComponentLibraryCollapseVisualState.Expanded,
expandedMargin,
expandedOpacity,
IsChipVisible: false);
}
public ComponentLibraryCollapseState WithVisualState(ComponentLibraryCollapseVisualState visualState, bool isChipVisible)
{
return this with
{
VisualState = visualState,
IsChipVisible = isChipVisible
};
}
}

View File

@@ -0,0 +1,9 @@
namespace LanMountainDesktop.DesktopEditing;
internal static class DesktopEditCommitMath
{
public static bool IsPendingCommitValid(bool isPending, int scheduledVersion, int currentVersion)
{
return isPending && scheduledVersion == currentVersion;
}
}

View File

@@ -0,0 +1,241 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
namespace LanMountainDesktop.DesktopEditing;
internal sealed class DesktopEditGhostView : Border
{
private static readonly TimeSpan FastDuration = TimeSpan.FromMilliseconds(120);
private static readonly Easing StandardEasing = new CubicEaseOut();
private readonly Border _accentDot;
private readonly TextBlock _titleTextBlock;
private readonly TextBlock _detailTextBlock;
private readonly Border _badgeBorder;
private readonly TextBlock _badgeTextBlock;
private readonly ScaleTransform _scaleTransform = new(1, 1);
private readonly SolidColorBrush _normalBackgroundBrush = new(Color.Parse("#F11B2430"));
private readonly SolidColorBrush _normalBorderBrush = new(Color.Parse("#4D8AA3C1"));
private readonly SolidColorBrush _normalAccentBrush = new(Color.Parse("#FF4F8EF7"));
private readonly SolidColorBrush _normalTextBrush = new(Color.Parse("#FFF5F7FA"));
private readonly SolidColorBrush _normalMutedTextBrush = new(Color.Parse("#BDE2E8F0"));
private readonly SolidColorBrush _normalBadgeBackgroundBrush = new(Color.Parse("#245E86D6"));
private readonly SolidColorBrush _normalBadgeBorderBrush = new(Color.Parse("#557EA7E6"));
private readonly SolidColorBrush _invalidBackgroundBrush = new(Color.Parse("#F01B1022"));
private readonly SolidColorBrush _invalidBorderBrush = new(Color.Parse("#FFE25555"));
private readonly SolidColorBrush _invalidAccentBrush = new(Color.Parse("#FFFF6B6B"));
private readonly SolidColorBrush _invalidBadgeBackgroundBrush = new(Color.Parse("#33FF4D4D"));
private readonly SolidColorBrush _invalidBadgeBorderBrush = new(Color.Parse("#88FF7676"));
public DesktopEditGhostView()
{
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalAlignment = VerticalAlignment.Stretch;
Padding = new Thickness(14);
Background = _normalBackgroundBrush;
BorderBrush = _normalBorderBrush;
BorderThickness = new Thickness(1);
CornerRadius = new CornerRadius(22);
ClipToBounds = true;
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
RenderTransform = _scaleTransform;
Transitions = new Transitions
{
new DoubleTransition
{
Property = Visual.OpacityProperty,
Duration = FastDuration,
Easing = StandardEasing
}
};
_scaleTransform.Transitions = new Transitions
{
new DoubleTransition
{
Property = ScaleTransform.ScaleXProperty,
Duration = FastDuration,
Easing = StandardEasing
},
new DoubleTransition
{
Property = ScaleTransform.ScaleYProperty,
Duration = FastDuration,
Easing = StandardEasing
}
};
_accentDot = new Border
{
Width = 10,
Height = 10,
CornerRadius = new CornerRadius(999),
Background = _normalAccentBrush,
BorderThickness = new Thickness(0),
VerticalAlignment = VerticalAlignment.Center
};
_titleTextBlock = new TextBlock
{
Foreground = _normalTextBrush,
FontWeight = FontWeight.SemiBold,
TextTrimming = TextTrimming.CharacterEllipsis,
TextWrapping = TextWrapping.NoWrap,
MaxLines = 1
};
_detailTextBlock = new TextBlock
{
Foreground = _normalMutedTextBrush,
TextTrimming = TextTrimming.CharacterEllipsis,
TextWrapping = TextWrapping.NoWrap,
MaxLines = 1
};
_badgeTextBlock = new TextBlock
{
Foreground = _normalTextBrush,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis,
TextWrapping = TextWrapping.NoWrap,
MaxLines = 1
};
_badgeBorder = new Border
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Padding = new Thickness(9, 4),
CornerRadius = new CornerRadius(999),
Background = _normalBadgeBackgroundBrush,
BorderBrush = _normalBadgeBorderBrush,
BorderThickness = new Thickness(1),
Child = _badgeTextBlock
};
var headerPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 8,
Children =
{
_accentDot,
_titleTextBlock
}
};
var contentPanel = new StackPanel
{
Spacing = 6,
Children =
{
headerPanel,
_detailTextBlock
}
};
var rootGrid = new Grid
{
RowDefinitions = new RowDefinitions
{
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Auto)
},
RowSpacing = 8
};
rootGrid.Children.Add(contentPanel);
rootGrid.Children.Add(_badgeBorder);
Grid.SetRow(contentPanel, 0);
Grid.SetRow(_badgeBorder, 1);
_badgeBorder.Margin = new Thickness(0, 2, 0, 0);
Child = rootGrid;
UpdatePreviewMetrics(180, 120);
UpdateContent(null, null, null);
}
public void UpdateContent(string? title, string? detail, string? badgeText)
{
_titleTextBlock.Text = string.IsNullOrWhiteSpace(title) ? "Component" : title;
_detailTextBlock.Text = string.IsNullOrWhiteSpace(detail) ? string.Empty : detail;
_detailTextBlock.IsVisible = !string.IsNullOrWhiteSpace(detail);
_badgeTextBlock.Text = string.IsNullOrWhiteSpace(badgeText) ? string.Empty : badgeText;
_badgeBorder.IsVisible = !string.IsNullOrWhiteSpace(badgeText);
}
public void UpdatePreviewMetrics(double width, double height)
{
var normalizedWidth = Math.Max(1, width);
var normalizedHeight = Math.Max(1, height);
var minSide = Math.Max(1, Math.Min(normalizedWidth, normalizedHeight));
CornerRadius = new CornerRadius(Math.Clamp(minSide * 0.16, 16, 28));
Padding = new Thickness(
Math.Clamp(minSide * 0.10, 10, 18),
Math.Clamp(minSide * 0.10, 10, 18),
Math.Clamp(minSide * 0.10, 10, 18),
Math.Clamp(minSide * 0.09, 10, 16));
var titleFontSize = Math.Clamp(minSide * 0.12, 12, 18);
var detailFontSize = Math.Clamp(minSide * 0.085, 10, 13);
var badgeFontSize = Math.Clamp(minSide * 0.08, 9, 12);
var dotSize = Math.Clamp(minSide * 0.07, 8, 12);
var badgeHorizontalPadding = Math.Clamp(minSide * 0.07, 8, 14);
var badgeVerticalPadding = Math.Clamp(minSide * 0.035, 3, 6);
_accentDot.Width = dotSize;
_accentDot.Height = dotSize;
_titleTextBlock.FontSize = titleFontSize;
_detailTextBlock.FontSize = detailFontSize;
_badgeTextBlock.FontSize = badgeFontSize;
_badgeBorder.Padding = new Thickness(badgeHorizontalPadding, badgeVerticalPadding);
}
public void SetInvalid(bool isInvalid)
{
if (isInvalid)
{
Background = _invalidBackgroundBrush;
BorderBrush = _invalidBorderBrush;
_accentDot.Background = _invalidAccentBrush;
_badgeBorder.Background = _invalidBadgeBackgroundBrush;
_badgeBorder.BorderBrush = _invalidBadgeBorderBrush;
_titleTextBlock.Foreground = _invalidBorderBrush;
_detailTextBlock.Foreground = _invalidBorderBrush;
_badgeTextBlock.Foreground = _invalidBorderBrush;
Opacity = 0.9;
return;
}
Background = _normalBackgroundBrush;
BorderBrush = _normalBorderBrush;
_accentDot.Background = _normalAccentBrush;
_badgeBorder.Background = _normalBadgeBackgroundBrush;
_badgeBorder.BorderBrush = _normalBadgeBorderBrush;
_titleTextBlock.Foreground = _normalTextBrush;
_detailTextBlock.Foreground = _normalMutedTextBrush;
_badgeTextBlock.Foreground = _normalTextBrush;
Opacity = 1.0;
}
public void SetRestingScale(double scale)
{
var clampedScale = Math.Clamp(scale, 0.85, 1.12);
_scaleTransform.ScaleX = clampedScale;
_scaleTransform.ScaleY = clampedScale;
}
public void AnimateToScale(double scale)
{
var clampedScale = Math.Clamp(scale, 0.85, 1.12);
_scaleTransform.ScaleX = clampedScale;
_scaleTransform.ScaleY = clampedScale;
}
}

View File

@@ -0,0 +1,287 @@
using System;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.DesktopEditing;
internal enum DesktopEditGhostVisualStyle
{
StandardLift = 0,
ElevatedFromLibrary
}
internal sealed class DesktopEditOverlayPresenter
{
private static readonly TimeSpan FastDuration = FluttermotionToken.Fast;
private static readonly Easing StandardEasing = new CubicEaseOut();
private readonly Canvas _root;
private readonly DesktopEditGhostView _ghostView;
private readonly Border _candidateOutline;
private readonly ScaleTransform _candidateScale = new(1, 1);
private Rect? _previewRect;
private Rect? _candidateRect;
private bool _isInvalid;
private bool _isVisible;
private int _dismissVersion;
private readonly SolidColorBrush _candidateBrush = new(Color.Parse("#FF4F8EF7"));
private readonly SolidColorBrush _candidateInvalidBrush = new(Color.Parse("#FFFF6B6B"));
private readonly SolidColorBrush _candidateFillBrush = new(Color.Parse("#224F8EF7"));
private readonly SolidColorBrush _candidateInvalidFillBrush = new(Color.Parse("#22FF6B6B"));
public DesktopEditOverlayPresenter()
{
_ghostView = new DesktopEditGhostView
{
IsHitTestVisible = false,
Opacity = 1
};
_candidateOutline = new Border
{
IsHitTestVisible = false,
Background = _candidateFillBrush,
BorderBrush = _candidateBrush,
BorderThickness = new Thickness(2),
CornerRadius = new CornerRadius(22),
Opacity = 0,
RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative),
RenderTransform = _candidateScale,
Transitions = new Transitions
{
new DoubleTransition
{
Property = Visual.OpacityProperty,
Duration = FastDuration,
Easing = StandardEasing
}
}
};
_candidateScale.Transitions = new Transitions
{
new DoubleTransition
{
Property = ScaleTransform.ScaleXProperty,
Duration = FastDuration,
Easing = StandardEasing
},
new DoubleTransition
{
Property = ScaleTransform.ScaleYProperty,
Duration = FastDuration,
Easing = StandardEasing
}
};
_candidateOutline.SetValue(Panel.ZIndexProperty, 0);
_ghostView.SetValue(Panel.ZIndexProperty, 1);
_root = new Canvas
{
IsHitTestVisible = false,
ClipToBounds = false,
Opacity = 0,
IsVisible = false,
Children =
{
_candidateOutline,
_ghostView
}
};
_root.Transitions = new Transitions
{
new DoubleTransition
{
Property = Visual.OpacityProperty,
Duration = FastDuration,
Easing = StandardEasing
}
};
}
public Control Root => _root;
public void SetViewportSize(Size size)
{
_root.Width = Math.Max(1, size.Width);
_root.Height = Math.Max(1, size.Height);
}
public void SetPreviewRect(Rect rect)
{
_previewRect = Normalize(rect);
ApplyPreviewRect();
}
public void SetCandidateRect(Rect? rect)
{
_candidateRect = rect is null ? null : Normalize(rect.Value);
ApplyCandidateRect();
}
public void UpdateGhostContent(string? title, string? detail = null, string? badge = null)
{
_ghostView.UpdateContent(title, detail, badge);
}
public void SetInvalid(bool isInvalid)
{
_isInvalid = isInvalid;
_ghostView.SetInvalid(isInvalid);
UpdateCandidateAppearance();
}
public void Show(DesktopEditGhostVisualStyle visualStyle = DesktopEditGhostVisualStyle.StandardLift)
{
_dismissVersion++;
_isVisible = true;
_root.IsVisible = true;
_root.Opacity = 0;
_ghostView.Opacity = 0;
var initialGhostScale = visualStyle == DesktopEditGhostVisualStyle.ElevatedFromLibrary ? 1.02 : 0.985;
var targetGhostScale = visualStyle == DesktopEditGhostVisualStyle.ElevatedFromLibrary ? 1.06 : 1;
_ghostView.SetRestingScale(initialGhostScale);
_candidateOutline.Opacity = 0;
_candidateScale.ScaleX = 0.96;
_candidateScale.ScaleY = 0.96;
Dispatcher.UIThread.Post(() =>
{
if (!_isVisible)
{
return;
}
_root.Opacity = 1;
_ghostView.Opacity = 1;
_ghostView.SetRestingScale(targetGhostScale);
if (_candidateRect.HasValue)
{
_candidateOutline.Opacity = 1;
_candidateScale.ScaleX = 1;
_candidateScale.ScaleY = 1;
}
}, DispatcherPriority.Background);
}
public void Hide()
{
_dismissVersion++;
_isVisible = false;
_root.Opacity = 0;
_ghostView.Opacity = 0;
_candidateOutline.Opacity = 0;
_candidateScale.ScaleX = 0.96;
_candidateScale.ScaleY = 0.96;
_ghostView.SetRestingScale(0.96);
_root.IsVisible = false;
}
public void Commit()
{
BeginDismiss(isCancel: false);
}
public void Cancel()
{
BeginDismiss(isCancel: true);
}
private void BeginDismiss(bool isCancel)
{
if (!_isVisible)
{
return;
}
var version = ++_dismissVersion;
_isVisible = false;
_candidateOutline.Opacity = 0;
_ghostView.Opacity = 0;
_root.Opacity = 0;
var targetScale = isCancel ? 0.96 : 1.04;
_ghostView.AnimateToScale(targetScale);
_candidateScale.ScaleX = targetScale;
_candidateScale.ScaleY = targetScale;
DispatcherTimer.RunOnce(
() =>
{
if (version != _dismissVersion)
{
return;
}
_root.IsVisible = false;
},
FastDuration);
}
private void ApplyPreviewRect()
{
if (!_previewRect.HasValue)
{
return;
}
var rect = _previewRect.Value;
_ghostView.Width = Math.Max(1, rect.Width);
_ghostView.Height = Math.Max(1, rect.Height);
Canvas.SetLeft(_ghostView, rect.X);
Canvas.SetTop(_ghostView, rect.Y);
_ghostView.UpdatePreviewMetrics(rect.Width, rect.Height);
}
private void ApplyCandidateRect()
{
if (!_candidateRect.HasValue)
{
_candidateOutline.IsVisible = false;
_candidateOutline.Opacity = 0;
return;
}
var rect = _candidateRect.Value;
_candidateOutline.IsVisible = true;
_candidateOutline.Width = Math.Max(1, rect.Width);
_candidateOutline.Height = Math.Max(1, rect.Height);
Canvas.SetLeft(_candidateOutline, rect.X);
Canvas.SetTop(_candidateOutline, rect.Y);
var cornerRadius = Math.Clamp(Math.Min(rect.Width, rect.Height) * 0.12, 14, 28);
_candidateOutline.CornerRadius = new CornerRadius(cornerRadius);
_candidateOutline.BorderBrush = _isInvalid ? _candidateInvalidBrush : _candidateBrush;
_candidateOutline.Background = _isInvalid ? _candidateInvalidFillBrush : _candidateFillBrush;
_candidateOutline.Opacity = _isVisible ? 1 : 0;
_candidateScale.ScaleX = _isVisible ? 1 : 0.96;
_candidateScale.ScaleY = _isVisible ? 1 : 0.96;
UpdateCandidateAppearance();
}
private void UpdateCandidateAppearance()
{
if (!_candidateRect.HasValue)
{
return;
}
_candidateOutline.BorderBrush = _isInvalid ? _candidateInvalidBrush : _candidateBrush;
_candidateOutline.Background = _isInvalid ? _candidateInvalidFillBrush : _candidateFillBrush;
}
private static Rect Normalize(Rect rect)
{
var width = Math.Max(1, rect.Width);
var height = Math.Max(1, rect.Height);
return new Rect(rect.X, rect.Y, width, height);
}
}

View File

@@ -0,0 +1,205 @@
using System;
using Avalonia;
namespace LanMountainDesktop.DesktopEditing;
internal enum DesktopEditSessionMode
{
None = 0,
PendingNew,
DraggingNew,
DraggingExisting,
ResizingExisting
}
internal readonly record struct DesktopEditSession
{
public DesktopEditSessionMode Mode { get; init; }
public string? ComponentId { get; init; }
public string? PlacementId { get; init; }
public int PageIndex { get; init; }
public int WidthCells { get; init; }
public int HeightCells { get; init; }
public Point StartPointerInViewport { get; init; }
public Point CurrentPointerInViewport { get; init; }
public Point PointerOffsetInViewport { get; init; }
public Rect? ComponentLibraryBounds { get; init; }
public int TargetRow { get; init; }
public int TargetColumn { get; init; }
public bool IsActive => Mode != DesktopEditSessionMode.None;
public bool IsPendingNew => Mode == DesktopEditSessionMode.PendingNew;
public bool IsDraggingNew => Mode == DesktopEditSessionMode.DraggingNew;
public bool IsDraggingExisting => Mode == DesktopEditSessionMode.DraggingExisting;
public bool IsResizingExisting => Mode == DesktopEditSessionMode.ResizingExisting;
public bool HasTargetCell => TargetRow >= 0 && TargetColumn >= 0;
public double PointerTravelDistance => DesktopPlacementMath.Distance(StartPointerInViewport, CurrentPointerInViewport);
public bool HasExceededThreshold(double threshold)
{
return DesktopPlacementMath.HasExceededThreshold(StartPointerInViewport, CurrentPointerInViewport, threshold);
}
public bool IsPointerInsideComponentLibrary()
{
return DesktopPlacementMath.IsOccludedByComponentLibrary(CurrentPointerInViewport, ComponentLibraryBounds);
}
public bool IsPreviewOccludedByComponentLibrary(Rect previewRect)
{
return DesktopPlacementMath.IsOccludedByComponentLibrary(previewRect, ComponentLibraryBounds);
}
public bool CanCommit => IsActive && HasTargetCell;
public Rect GetPreviewRect(DesktopGridGeometry grid)
{
if (HasTargetCell)
{
return DesktopPlacementMath.GetCellRect(
grid,
TargetColumn,
TargetRow,
Math.Max(1, WidthCells),
Math.Max(1, HeightCells));
}
var freePreviewOrigin = DesktopPlacementMath.Subtract(CurrentPointerInViewport, PointerOffsetInViewport);
return new Rect(
freePreviewOrigin,
new Size(
Math.Max(1, WidthCells) * grid.CellSize + Math.Max(0, Math.Max(1, WidthCells) - 1) * grid.CellGap,
Math.Max(1, HeightCells) * grid.CellSize + Math.Max(0, Math.Max(1, HeightCells) - 1) * grid.CellGap));
}
public DesktopEditSession WithCurrentPointer(Point pointerInViewport)
{
return this with { CurrentPointerInViewport = pointerInViewport };
}
public DesktopEditSession WithComponentLibraryBounds(Rect? componentLibraryBounds)
{
return this with { ComponentLibraryBounds = componentLibraryBounds };
}
public DesktopEditSession WithTargetCell(int row, int column)
{
return this with { TargetRow = row, TargetColumn = column };
}
public DesktopEditSession PromoteToDraggingNew()
{
return this with { Mode = DesktopEditSessionMode.DraggingNew };
}
public DesktopEditSession PromoteToDraggingExisting()
{
return this with { Mode = DesktopEditSessionMode.DraggingExisting };
}
public DesktopEditSession PromoteToResizingExisting()
{
return this with { Mode = DesktopEditSessionMode.ResizingExisting };
}
public static DesktopEditSession CreatePendingNew(
string componentId,
int pageIndex,
int widthCells,
int heightCells,
Point startPointerInViewport,
Point pointerOffsetInViewport,
Rect? componentLibraryBounds)
{
return new DesktopEditSession
{
Mode = DesktopEditSessionMode.PendingNew,
ComponentId = componentId,
PageIndex = pageIndex,
WidthCells = Math.Max(1, widthCells),
HeightCells = Math.Max(1, heightCells),
StartPointerInViewport = startPointerInViewport,
CurrentPointerInViewport = startPointerInViewport,
PointerOffsetInViewport = pointerOffsetInViewport,
ComponentLibraryBounds = componentLibraryBounds,
TargetRow = -1,
TargetColumn = -1
};
}
public static DesktopEditSession CreateDraggingNew(
string componentId,
int pageIndex,
int widthCells,
int heightCells,
Point startPointerInViewport,
Point pointerOffsetInViewport,
Rect? componentLibraryBounds)
{
return CreatePendingNew(
componentId,
pageIndex,
widthCells,
heightCells,
startPointerInViewport,
pointerOffsetInViewport,
componentLibraryBounds) with
{
Mode = DesktopEditSessionMode.DraggingNew
};
}
public static DesktopEditSession CreateDraggingExisting(
string componentId,
string placementId,
int pageIndex,
int widthCells,
int heightCells,
Point startPointerInViewport,
Point pointerOffsetInViewport,
Rect? componentLibraryBounds)
{
return new DesktopEditSession
{
Mode = DesktopEditSessionMode.DraggingExisting,
ComponentId = componentId,
PlacementId = placementId,
PageIndex = pageIndex,
WidthCells = Math.Max(1, widthCells),
HeightCells = Math.Max(1, heightCells),
StartPointerInViewport = startPointerInViewport,
CurrentPointerInViewport = startPointerInViewport,
PointerOffsetInViewport = pointerOffsetInViewport,
ComponentLibraryBounds = componentLibraryBounds,
TargetRow = -1,
TargetColumn = -1
};
}
public static DesktopEditSession CreateResizingExisting(
string componentId,
string placementId,
int pageIndex,
int widthCells,
int heightCells,
Point startPointerInViewport,
Rect? componentLibraryBounds)
{
return new DesktopEditSession
{
Mode = DesktopEditSessionMode.ResizingExisting,
ComponentId = componentId,
PlacementId = placementId,
PageIndex = pageIndex,
WidthCells = Math.Max(1, widthCells),
HeightCells = Math.Max(1, heightCells),
StartPointerInViewport = startPointerInViewport,
CurrentPointerInViewport = startPointerInViewport,
PointerOffsetInViewport = default,
ComponentLibraryBounds = componentLibraryBounds,
TargetRow = -1,
TargetColumn = -1
};
}
}

View File

@@ -0,0 +1,176 @@
using System;
using Avalonia;
namespace LanMountainDesktop.DesktopEditing;
internal readonly record struct DesktopGridGeometry(
Point Origin,
double CellSize,
double CellGap,
int ColumnCount,
int RowCount)
{
public double Pitch => CellSize + CellGap;
public bool IsValid =>
CellSize > 0 &&
ColumnCount > 0 &&
RowCount > 0 &&
Pitch > 0;
}
internal static class DesktopPlacementMath
{
public static double ComputeDragStartThreshold(double cellSize)
{
return Math.Max(10d, Math.Max(0d, cellSize) * 0.18d);
}
public static double Distance(Point start, Point end)
{
return Math.Sqrt(DistanceSquared(start, end));
}
public static double DistanceSquared(Point start, Point end)
{
var deltaX = end.X - start.X;
var deltaY = end.Y - start.Y;
return deltaX * deltaX + deltaY * deltaY;
}
public static bool HasExceededThreshold(Point start, Point end, double threshold)
{
if (threshold <= 0)
{
return true;
}
return DistanceSquared(start, end) >= threshold * threshold;
}
public static Point Add(Point left, Point right)
{
return new Point(left.X + right.X, left.Y + right.Y);
}
public static Point Subtract(Point left, Point right)
{
return new Point(left.X - right.X, left.Y - right.Y);
}
public static bool ContainsPoint(Rect rect, Point point)
{
return rect.Contains(point);
}
public static bool Intersects(Rect left, Rect right)
{
return left.Intersects(right);
}
public static bool HasCellPositionChanged(int originalRow, int originalColumn, int targetRow, int targetColumn)
{
return originalRow != targetRow || originalColumn != targetColumn;
}
public static bool HasCellSpanChanged(int originalWidthCells, int originalHeightCells, int targetWidthCells, int targetHeightCells)
{
return originalWidthCells != targetWidthCells || originalHeightCells != targetHeightCells;
}
public static bool IsOccludedByComponentLibrary(Point point, Rect? componentLibraryBounds)
{
return componentLibraryBounds.HasValue && ContainsPoint(componentLibraryBounds.Value, point);
}
public static bool IsOccludedByComponentLibrary(Rect previewRect, Rect? componentLibraryBounds)
{
return componentLibraryBounds.HasValue && Intersects(previewRect, componentLibraryBounds.Value);
}
public static bool CanCommitPlacement(Rect placementRect, Rect? componentLibraryBounds)
{
return !IsOccludedByComponentLibrary(placementRect, componentLibraryBounds);
}
public static Rect GetGridBounds(DesktopGridGeometry grid)
{
if (!grid.IsValid)
{
return default;
}
var width = grid.ColumnCount * grid.CellSize + Math.Max(0, grid.ColumnCount - 1) * grid.CellGap;
var height = grid.RowCount * grid.CellSize + Math.Max(0, grid.RowCount - 1) * grid.CellGap;
return new Rect(grid.Origin, new Size(width, height));
}
public static Rect GetCellRect(
DesktopGridGeometry grid,
int column,
int row,
int widthCells = 1,
int heightCells = 1)
{
var safeWidthCells = Math.Max(1, widthCells);
var safeHeightCells = Math.Max(1, heightCells);
var safeColumn = Math.Max(0, column);
var safeRow = Math.Max(0, row);
var pitch = grid.Pitch;
var x = grid.Origin.X + safeColumn * pitch;
var y = grid.Origin.Y + safeRow * pitch;
var width = safeWidthCells * grid.CellSize + Math.Max(0, safeWidthCells - 1) * grid.CellGap;
var height = safeHeightCells * grid.CellSize + Math.Max(0, safeHeightCells - 1) * grid.CellGap;
return new Rect(x, y, width, height);
}
public static Rect GetSnappedCellRect(
DesktopGridGeometry grid,
Point pointerInViewport,
Point pointerOffset,
int widthCells,
int heightCells)
{
return TryGetSnappedCell(grid, pointerInViewport, pointerOffset, widthCells, heightCells, out var column, out var row)
? GetCellRect(grid, column, row, widthCells, heightCells)
: default;
}
public static bool TryGetSnappedCell(
DesktopGridGeometry grid,
Point pointerInViewport,
Point pointerOffset,
int widthCells,
int heightCells,
out int column,
out int row)
{
column = 0;
row = 0;
if (!grid.IsValid)
{
return false;
}
var safeWidthCells = Math.Max(1, widthCells);
var safeHeightCells = Math.Max(1, heightCells);
var maxColumn = Math.Max(0, grid.ColumnCount - safeWidthCells);
var maxRow = Math.Max(0, grid.RowCount - safeHeightCells);
var pitch = grid.Pitch;
if (pitch <= 0)
{
return false;
}
var previewOrigin = Subtract(pointerInViewport, pointerOffset);
var relativeX = previewOrigin.X - grid.Origin.X;
var relativeY = previewOrigin.Y - grid.Origin.Y;
column = (int)Math.Floor(relativeX / pitch);
row = (int)Math.Floor(relativeY / pitch);
column = Math.Clamp(column, 0, maxColumn);
row = Math.Clamp(row, 0, maxRow);
return true;
}
}

View File

@@ -38,6 +38,27 @@
"settings.wallpaper.title": "Wallpaper",
"settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.",
"settings.wallpaper.current_label": "Current Wallpaper",
"settings.wallpaper.type_label": "Wallpaper Type",
"settings.wallpaper.type.image": "Image",
"settings.wallpaper.type.solid_color": "Solid Color",
"settings.wallpaper.type.system": "System Wallpaper",
"settings.wallpaper.system.label": "System Wallpaper",
"settings.wallpaper.system.unavailable": "Unable to read system wallpaper",
"settings.wallpaper.refresh_interval": "Refresh Interval",
"settings.wallpaper.refresh_now": "Refresh Now",
"settings.wallpaper.refresh.30s": "30 seconds",
"settings.wallpaper.refresh.1m": "1 minute",
"settings.wallpaper.refresh.5m": "5 minutes",
"settings.wallpaper.refresh.10m": "10 minutes",
"settings.wallpaper.refresh.15m": "15 minutes",
"settings.wallpaper.refresh.30m": "30 minutes",
"settings.wallpaper.refresh.1h": "1 hour",
"settings.wallpaper.refresh.2h": "2 hours",
"settings.wallpaper.refresh.4h": "4 hours",
"settings.wallpaper.refresh.8h": "8 hours",
"settings.wallpaper.refresh.12h": "12 hours",
"settings.wallpaper.refresh.24h": "24 hours",
"settings.wallpaper.color_label": "Wallpaper Color",
"settings.wallpaper.placement_label": "Placement",
"settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.",
"settings.wallpaper.pick_button": "Browse Files",
@@ -217,7 +238,14 @@
"schedule.settings.unnamed": "Unnamed Schedule",
"schedule.settings.delete": "Delete",
"schedule.settings.picker_title": "Select ClassIsland schedule file",
"schedule.settings.picker_file_type": "ClassIsland CSES schedule",
"schedule.settings.picker_file_type.all": "ClassIsland Schedule Files",
"schedule.settings.picker_file_type.json": "ClassIsland Profile (JSON)",
"schedule.settings.picker_file_type.cses": "CSES Schedule (YAML)",
"schedule.settings.semester.title": "Semester Settings",
"schedule.settings.semester.start_date": "Semester Start Date",
"schedule.settings.semester.week_cycle": "Week Cycle",
"schedule.settings.semester.week_cycle_desc": "Set the week rotation cycle for multi-week schedules (e.g., 2 for odd/even weeks).",
"schedule.settings.semester.week_cycle_format": "{0}-week rotation",
"worldclock.settings.title": "World Clock Settings",
"worldclock.settings.desc": "Choose a time zone for each of the four clocks.",
"worldclock.settings.clock_1": "Clock 1",
@@ -248,6 +276,7 @@
"settings.region.language_label": "Language",
"settings.region.language_zh": "Chinese",
"settings.region.language_en": "English",
"settings.region.language_ja": "Japanese",
"settings.region.timezone_header": "Time Zone",
"settings.region.timezone_desc": "Select a time zone. Clock and calendar widgets will follow this zone.",
"settings.region.applied_format": "Language switched to: {0}",

View File

@@ -0,0 +1,973 @@
{
"app.title": "LanMountainDesktop",
"tray.tooltip": "LanMountainDesktop",
"tray.menu.show_desktop": "デスクトップを開く",
"tray.menu.settings": "設定",
"tray.menu.component_library": "ウィジェットライブラリ",
"tray.menu.restart": "アプリを再起動",
"tray.menu.exit": "アプリを終了",
"button.back_to_windows": "Windowsに戻る",
"button.back_to_platform": "{0}に戻る",
"tooltip.back_to_windows": "Windowsに戻る",
"tooltip.back_to_platform": "{0}に戻る",
"platform.windows": "Windows",
"platform.linux": "Linux",
"platform.macos": "macOS",
"tooltip.open_settings": "設定",
"settings.title": "設定",
"settings.shell.title": "設定",
"settings.shell.subtitle": "LanMountainDesktop 独立設定モジュール",
"settings.shell.sidebar_hint": "カテゴリを選択して、アプリの動作、デスクトップレイアウト、外観を調整します。",
"settings.shell.footer_hint": "トレイから開く設定は、この独立設定モジュールで管理されます。",
"settings.back_to_desktop": "デスクトップに戻る",
"settings.nav_header": "設定",
"settings.nav.group_desktop": "デスクトップ",
"settings.nav.group_system": "システム",
"settings.nav.group_extensions": "拡張機能",
"settings.nav.wallpaper": "壁紙",
"settings.nav.grid": "コンポーネント",
"settings.nav.color": "カラー",
"settings.nav.status_bar": "ステータスバー",
"settings.nav.weather": "天気",
"settings.nav.region": "地域",
"settings.nav.update": "アップデート",
"settings.nav.privacy": "プライバシー",
"settings.nav.launcher": "アプリランチャー",
"settings.nav.plugins": "プラグイン",
"settings.nav.about": "について",
"settings.wallpaper.title": "壁紙",
"settings.wallpaper.description": "画像または動画を選択して、アプリウィンドウの壁紙としてすぐに適用します。",
"settings.wallpaper.current_label": "現在の壁紙",
"settings.wallpaper.type_label": "壁紙タイプ",
"settings.wallpaper.type.image": "画像",
"settings.wallpaper.type.solid_color": "単色",
"settings.wallpaper.type.system": "システム壁紙",
"settings.wallpaper.system.label": "システム壁紙",
"settings.wallpaper.system.unavailable": "システム壁紙を読み込めません",
"settings.wallpaper.refresh_interval": "更新間隔",
"settings.wallpaper.refresh_now": "今すぐ更新",
"settings.wallpaper.refresh.30s": "30秒",
"settings.wallpaper.refresh.1m": "1分",
"settings.wallpaper.refresh.5m": "5分",
"settings.wallpaper.refresh.10m": "10分",
"settings.wallpaper.refresh.15m": "15分",
"settings.wallpaper.refresh.30m": "30分",
"settings.wallpaper.refresh.1h": "1時間",
"settings.wallpaper.refresh.2h": "2時間",
"settings.wallpaper.refresh.4h": "4時間",
"settings.wallpaper.refresh.8h": "8時間",
"settings.wallpaper.refresh.12h": "12時間",
"settings.wallpaper.refresh.24h": "24時間",
"settings.wallpaper.color_label": "壁紙の色",
"settings.wallpaper.placement_label": "配置",
"settings.wallpaper.placement_desc": "画像がデスクトップにどのように表示されるかを調整します。",
"settings.wallpaper.pick_button": "ファイルを参照",
"settings.wallpaper.clear_button": "単色にリセット",
"settings.wallpaper.no_selection": "壁紙が選択されていません。",
"settings.wallpaper.storage_unavailable": "ストレージプロバイダが利用できません。",
"settings.wallpaper.import_failed": "壁紙ファイルのインポートに失敗しました。",
"settings.wallpaper.image_applied": "画像の壁紙が適用されました。",
"settings.wallpaper.video_applied": "動画の壁紙が適用されました。",
"settings.wallpaper.unsupported_file": "選択されたファイルタイプはサポートされていません。",
"settings.wallpaper.apply_failed_format": "壁紙の適用に失敗しました: {0}",
"settings.wallpaper.mode_format": "壁紙モード: {0}。",
"settings.wallpaper.video_mode": "動画の壁紙は自動フィルモードを使用します。",
"settings.wallpaper.cleared": "背景が単色にリセットされました。",
"settings.wallpaper.default_status": "現在の背景は単色を使用しています。",
"settings.wallpaper.saved_not_found": "保存された壁紙ファイルが見つかりません。単色の背景を使用しています。",
"settings.wallpaper.restored": "保存された設定から壁紙が復元されました。",
"settings.wallpaper.video_restored": "保存された設定から動画の壁紙が復元されました。",
"settings.wallpaper.restore_failed": "保存された壁紙の復元に失敗しました。単色の背景を使用しています。",
"settings.wallpaper.video_not_found": "動画の壁紙ファイルが見つかりません。",
"settings.wallpaper.video_player_unavailable": "動画プレーヤーが利用できません。",
"settings.wallpaper.video_play_failed_format": "動画の壁紙の再生に失敗しました: {0}",
"settings.grid.title": "グリッドレイアウト",
"settings.grid.description": "すべてのコンポーネントは少なくとも1つのセルを占有する必要があります最小1x1。",
"settings.grid.short_side_label": "短辺のセル数",
"settings.grid.spacing_label": "グリッドの間隔",
"settings.grid.spacing_relaxed": "ゆとりありiOS",
"settings.grid.spacing_compact": "コンパクトAndroid",
"settings.grid.edge_inset_label": "画面の余白",
"settings.grid.edge_inset_px_format": "≈ {0:F1}px",
"settings.grid.apply_button": "適用",
"settings.grid.info_format": "グリッド: {0}列 x {1}行 | セル {2:F1}px (1:1)",
"settings.color.title": "カラー",
"settings.color.description": "昼夜モードを切り替え、アプリのアクセントカラーを選択します。",
"settings.color.day_night_label": "昼夜モード",
"settings.color.day_night_on": "夜",
"settings.color.day_night_off": "昼",
"settings.color.recommended_label": "おすすめの色",
"settings.color.system_monet_label": "システムMonetカラー",
"settings.color.refresh_button": "更新",
"settings.color.mode_night": "夜モードが有効",
"settings.color.mode_day": "昼モードが有効",
"settings.color.mode_status_format": "テーマモード: {0}。",
"settings.color.monet_refreshed": "Monetカラーが更新されました。",
"settings.color.theme_ready_format": "テーマカラーの準備完了: {0}。",
"settings.color.theme_applied_format": "{0}カラーが適用されました: {1}。",
"settings.color.theme_updated_wallpaper": "壁紙が更新されました。Monetカラーが更新されました。",
"settings.color.theme_updated_video": "動画の壁紙が更新されました。テーマカラーが更新されました。",
"settings.color.theme_cleared_wallpaper": "壁紙がクリアされました。Monetカラーが更新されました。",
"settings.status_bar.title": "ステータスバー",
"settings.status_bar.description": "上部のステータスバーに表示するコンポーネントを選択します。",
"settings.status_bar.clock_header": "時計コンポーネント",
"settings.status_bar.clock_description": "上部のステータスバーに時計を表示します。",
"settings.status_bar.clock_transparent_background_label": "透明な背景",
"settings.status_bar.clock_transparent_background_desc": "カプセルの背景を削除し、時計のテキストのみを保持します。",
"settings.status_bar.spacing_header": "コンポーネントの間隔",
"settings.status_bar.spacing_desc": "ステータスバーコンポーネント間の間隔を調整します。",
"settings.status_bar.spacing_mode_compact": "コンパクト",
"settings.status_bar.spacing_mode_relaxed": "ゆとりあり",
"settings.status_bar.spacing_mode_custom": "カスタム",
"settings.status_bar.spacing_custom_label": "カスタム間隔(%",
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
"settings.privacy.title": "プライバシー",
"settings.privacy.description": "アプリの改善に役立つオプションの匿名アップロードを管理します。",
"settings.privacy.crash_upload_title": "匿名クラッシュデータのアップロード",
"settings.privacy.crash_upload_description": "アプリケーションの安定性向上にご協力ください。",
"settings.privacy.usage_upload_title": "匿名使用データのアップロード",
"settings.privacy.usage_upload_description": "アプリケーション機能の改善にご協力ください。",
"settings.privacy.device_id_title": "デバイスID",
"settings.privacy.device_id_description": "このデバイスの一意識別子。更新をクリックして再生成します。",
"settings.privacy.refresh_device_id": "更新",
"settings.privacy.policy_hint_prefix": "詳細については、",
"settings.privacy.view_policy": "プライバシーポリシーをご覧ください",
"settings.weather.title": "天気",
"settings.weather.description": "天気の場所、Xiaomi天気プレビュー、起動時の位置情報取得動作を設定します。",
"settings.weather.location_source_header": "位置情報ソース",
"settings.weather.location_source_desc": "天気ウィジェットが場所を解決する方法を選択します。",
"settings.weather.mode_city_search": "都市検索",
"settings.weather.mode_coordinates": "座標",
"settings.weather.auto_refresh": "起動時に位置情報を自動更新",
"settings.weather.city_search_header": "都市検索",
"settings.weather.city_search_desc": "都市を検索し、天気の場所を適用します。",
"settings.weather.search_placeholder": "例: 東京",
"settings.weather.search_button": "検索",
"settings.weather.apply_city_button": "都市を適用",
"settings.weather.search_hint": "都市名で検索し、場所を適用します。",
"settings.weather.search_required": "都市のキーワードを入力してください。",
"settings.weather.search_no_results": "場所が見つかりませんでした。",
"settings.weather.search_failed_format": "検索に失敗しました: {0}",
"settings.weather.search_result_count_format": "{0}件の場所が見つかりました。",
"settings.weather.search_select_required": "検索結果から場所を1つ選択してください。",
"settings.weather.search_applied_format": "場所が適用されました: {0}",
"settings.weather.coordinates_header": "座標",
"settings.weather.coordinates_desc": "緯度/経度とオプションのキー/名前を設定します。",
"settings.weather.latitude_label": "緯度",
"settings.weather.longitude_label": "経度",
"settings.weather.location_key_placeholder": "場所キー(オプション)",
"settings.weather.location_name_placeholder": "表示名(オプション)",
"settings.weather.apply_coordinates_button": "座標を適用",
"settings.weather.coordinates_saved_format": "座標が保存されました: {0:F4}, {1:F4}",
"settings.weather.coordinates_default_name_format": "座標 {0:F4}, {1:F4}",
"settings.weather.location_services_header": "位置情報サービス",
"settings.weather.location_services_desc": "現在のWindowsの場所を使用し、起動時に自動的に更新するかどうかを決定します。",
"settings.weather.use_current_location": "現在地を使用",
"settings.weather.location_unsupported": "現在のプラットフォームは現在地の取得をサポートしていません。",
"settings.weather.location_ready": "現在のWindowsの場所を使用できます。",
"settings.weather.location_refreshing": "現在地を取得中...",
"settings.weather.location_refresh_success_format": "現在地が適用されました: {0}",
"settings.weather.location_refresh_failed_format": "現在地の取得に失敗しました: {0}",
"settings.weather.preview_header": "接続テスト",
"settings.weather.preview_desc": "テストリクエストを送信して現在の設定を確認します。",
"settings.weather.preview_button": "テスト取得",
"settings.weather.preview_section": "天気プレビュー",
"settings.weather.settings_section": "設定",
"settings.weather.preview_panel_header": "天気プレビュー",
"settings.weather.preview_panel_desc": "現在の天気サービスの状態を更新して確認します。",
"settings.weather.refresh_button": "更新",
"settings.weather.preview_updated_format": "{0}に更新",
"settings.weather.preview_hint": "テスト取得を使用して天気の設定を確認します。",
"settings.weather.preview_missing_location": "テストする前に天気の場所を適用してください。",
"settings.weather.preview_success_format": "テスト成功: {0} · {1} · {2}",
"settings.weather.preview_failed_format": "テスト取得に失敗しました: {0}",
"settings.weather.preview_unknown": "不明",
"settings.weather.alert_filter_header": "除外するアラート",
"settings.weather.alert_filter_desc": "これらの単語を含むアラートは表示されません。1行に1つのルール。",
"settings.weather.alert_filter_placeholder": "1行に1つのキーワード",
"settings.weather.icon_style_header": "天気アイコンスタイル",
"settings.weather.icon_style_desc": "天気シンボルのFluentアイコンスタイルを選択します。",
"settings.weather.icon_style_fluent_regular": "Fluent Regular",
"settings.weather.icon_style_fluent_filled": "Fluent Filled",
"settings.weather.no_tls_header": "TLSなしの天気リクエスト",
"settings.weather.no_tls_desc": "推奨されません。互換性のないネットワーク環境でのみ有効にしてください。",
"settings.weather.status_city_empty": "都市の場所が設定されていません。",
"settings.weather.status_city_format": "モード: {0} | {1} | キー: {2}",
"settings.weather.status_coordinates_format": "モード: {0} | 緯度 {1:F4}, 経度 {2:F4} | キー: {3}",
"settings.weather.city_selection_label": "都市選択",
"settings.weather.coordinates_selection_label": "座標の場所",
"settings.weather.location_city_summary_desc": "天気の照会に使用される現在の都市を選択します。",
"settings.weather.location_coordinates_summary_desc": "天気の照会に使用される緯度/経度とオプションの場所名を設定します。",
"settings.weather.location_not_selected": "場所が選択されていません",
"settings.weather.alert_list_label": "除外リスト",
"settings.weather.alert_list_desc": "1行に1つの除外ルール。",
"settings.weather.no_tls_toggle": "非TLSリクエストのフォールバックを許可",
"settings.weather.footer_hint": "デスクトップ天気ウィジェットは、ここで設定された場所とアラート除外設定を再利用します。",
"settings.weather.location_header": "天気の場所",
"settings.weather.location_desc": "天気ウィジェットで使用する場所を設定します。",
"settings.weather.location_placeholder": "例: 東京",
"settings.weather.location_apply": "保存",
"settings.weather.location_empty": "天気の場所が設定されていません。",
"settings.weather.location_required": "天気の場所は空にできません。",
"settings.weather.location_current_format": "現在の天気の場所: {0}",
"settings.weather.location_saved_format": "天気の場所が保存されました: {0}",
"weather.widget.location_not_configured": "天気の場所が設定されていません",
"weather.widget.configure_hint": "設定 > 天気を開いて設定",
"weather.widget.loading": "読み込み中...",
"weather.widget.fetch_failed": "天気の取得に失敗しました",
"weather.widget.retrying": "自動的に再試行中",
"weather.widget.location_unknown": "不明な場所",
"weather.widget.condition_clear": "晴れ",
"weather.widget.condition_cloudy": "曇り",
"weather.widget.condition_rain": "雨",
"weather.widget.condition_storm": "雷雨",
"weather.widget.condition_snow": "雪",
"weather.widget.condition_fog": "霧",
"weather.widget.condition_unknown": "不明",
"weather.widget.range_unknown": "-- / --",
"weather.widget.range_format": "{0} / {1}",
"schedule.widget.no_source": "ClassIslandのスケジュールデータが見つかりません",
"schedule.widget.no_class_today": "今日の授業はありません",
"schedule.widget.layout_missing": "スケジュールの時間レイアウトがありません",
"schedule.widget.subject_fallback": "無題の授業",
"schedule.widget.detail_fallback": "詳細なし",
"schedule.settings.title": "スケジュールのインポート",
"schedule.settings.desc": "ClassIsland CSESスケジュールをインポートし、有効にするものを選択します。",
"schedule.settings.add": "スケジュールを追加",
"schedule.settings.empty": "インポートされたスケジュールはありません",
"schedule.settings.unnamed": "無題のスケジュール",
"schedule.settings.delete": "削除",
"schedule.settings.picker_title": "ClassIslandスケジュールファイルを選択",
"schedule.settings.picker_file_type.all": "ClassIslandスケジュールファイル",
"schedule.settings.picker_file_type.json": "ClassIslandプロファイルJSON",
"schedule.settings.picker_file_type.cses": "CSESスケジュールYAML",
"schedule.settings.semester.title": "学期設定",
"schedule.settings.semester.start_date": "学期開始日",
"schedule.settings.semester.week_cycle": "週サイクル",
"schedule.settings.semester.week_cycle_desc": "複数週スケジュールの週ローテーションサイクルを設定します(例: 奇数週/偶数週の場合は2。",
"schedule.settings.semester.week_cycle_format": "{0}週ローテーション",
"worldclock.settings.title": "世界時計の設定",
"worldclock.settings.desc": "4つの時計それぞれのタイムゾーンを選択します。",
"worldclock.settings.clock_1": "時計 1",
"worldclock.settings.clock_2": "時計 2",
"worldclock.settings.clock_3": "時計 3",
"worldclock.settings.clock_4": "時計 4",
"worldclock.settings.second_mode_label": "秒針",
"worldclock.widget.today": "今日",
"worldclock.widget.yesterday": "昨日",
"worldclock.widget.tomorrow": "明日",
"worldclock.widget.offset_same": "0時間",
"worldclock.widget.offset_ahead_hours": "{0}時間進む",
"worldclock.widget.offset_behind_hours": "{0}時間遅れる",
"worldclock.widget.offset_ahead_hm": "{0}時間{1}分進む",
"worldclock.widget.offset_behind_hm": "{0}時間{1}分遅れる",
"weather.widget.aqi_unknown": "AQI --",
"weather.widget.aqi_format": "AQI {0}",
"weather.widget.updated_format": "{0:HH:mm}に更新",
"weather.hourly.now": "現在",
"weather.hourly.sunset": "日没",
"weather.multiday.today": "今日",
"weather.multiday.tomorrow": "明日",
"weather.multiday.aqi_format": "空気質 {0}",
"weather.multiday.aqi_unknown": "空気質 --",
"settings.region.title": "地域",
"settings.region.description": "言語を選択し、設定と主要なUIにすぐに適用します。",
"settings.region.language_header": "言語",
"settings.region.language_label": "言語",
"settings.region.language_zh": "中国語",
"settings.region.language_en": "英語",
"settings.region.language_ja": "日本語",
"settings.region.timezone_header": "タイムゾーン",
"settings.region.timezone_desc": "タイムゾーンを選択します。時計とカレンダーウィジェットはこのゾーンに従います。",
"settings.region.applied_format": "言語が切り替わりました: {0}",
"settings.region.follow_system": "システムの既定に従う",
"settings.general.title": "一般",
"settings.general.description": "言語、タイムゾーン、ランタイムの動作を調整します。",
"settings.general.basic_header": "基本設定",
"settings.general.runtime_header": "ランタイム",
"settings.general.preview_header": "日時プレビュー",
"settings.general.preview_time_label": "時刻",
"settings.general.preview_date_label": "日付",
"settings.general.render_mode_restart_message": "レンダリングモードの変更にはアプリの再起動が必要です。",
"settings.appearance.title": "外観",
"settings.appearance.description": "テーマソース、システムマテリアル、ウィンドウクロームを調整します。",
"settings.appearance.theme_header": "テーマ",
"settings.color.enable_night_mode_toggle": "夜モードを有効にする",
"settings.color.use_system_chrome_toggle": "システムのウィンドウクロームを使用",
"settings.color.theme_color_label": "テーマのアクセントカラー",
"settings.appearance.theme_color_mode_label": "テーマカラーソース",
"settings.appearance.theme_color_mode.neutral": "デフォルトニュートラル",
"settings.appearance.theme_color_mode.user": "ユーザーテーマカラーMonet",
"settings.appearance.theme_color_mode.wallpaper": "壁紙Monet",
"settings.appearance.theme_color_mode_desc.neutral": "ライトモードとダークモードにデフォルトの白と黒のニュートラルサーフェスを使用します。",
"settings.appearance.theme_color_mode_desc.user": "選択したテーマカラーをシェル全体のMonetシードとして使用します。",
"settings.appearance.theme_color_mode_desc.wallpaper": "壁紙の色を使用します。アプリの壁紙が優先され、次にシステムの壁紙が使用されます。",
"settings.appearance.theme_color_preview.app": "現在、アプリの壁紙から抽出された色をプレビューしています。",
"settings.appearance.theme_color_preview.system": "現在、システムの壁紙から抽出された色をプレビューしています。",
"settings.appearance.theme_color_preview.fallback": "使用可能な壁紙が見つかりませんでした。アプリはフォールバックのアクセントを使用しています。",
"component.color_scheme.follow_system": "システムのカラースキームに従う",
"component.color_scheme.native": "コンポーネントのカスタムカラースキームを使用",
"settings.appearance.system_material.none": "なし",
"settings.appearance.system_material.mica": "Mica",
"settings.appearance.system_material.acrylic": "Acrylic",
"settings.appearance.system_material_desc.switchable": "選択したマテリアルをウィンドウ、Dock、ステータスバー、コンポーネントホストに適用します。",
"settings.appearance.system_material_desc.fixed": "現在のシステムは、ここにリストされているマテリアルモードのみを公開しています。",
"settings.appearance.restart_message": "テーマソースとシステムマテリアルの変更にはアプリの再起動が必要です。",
"settings.appearance.preview.primary": "プライマリ",
"settings.appearance.preview.secondary": "セカンダリ",
"settings.appearance.preview.tertiary": "ターシャリ",
"settings.appearance.preview.neutral": "ニュートラル",
"settings.appearance.preview.seed": "シード",
"settings.appearance.preview.neutral_light": "白",
"settings.appearance.preview.neutral_dark": "黒",
"settings.appearance.preview.apply_seed": "適用",
"settings.appearance.preview.wallpaper_candidates": "壁紙シード候補",
"settings.appearance.preview.wallpaper_current": "現在",
"settings.wallpaper.placement.fill": "フィル",
"settings.wallpaper.placement.fit": "フィット",
"settings.wallpaper.placement.stretch": "ストレッチ",
"settings.wallpaper.placement.center": "中央",
"settings.wallpaper.placement.tile": "タイル",
"settings.status_bar.clock_format_label": "時計の形式",
"settings.status_bar.clock_format.hm": "時:分",
"settings.status_bar.clock_format.hms": "時:分:秒",
"settings.components.title": "コンポーネント",
"settings.components.description": "コンポーネントのレイアウトとコーナーデザインを調整します。",
"settings.components.grid_header": "グリッド設定",
"settings.components.header": "グリッド設定",
"settings.components.short_side_label": "短辺のセル数",
"settings.components.edge_inset_label": "画面の余白",
"settings.components.spacing_label": "コンポーネントの間隔",
"settings.components.spacing_compact": "コンパクト",
"settings.components.spacing_relaxed": "ゆとりあり",
"settings.components.corner_radius.header": "コーナーデザイン",
"settings.components.corner_radius.label": "コンポーネントのコーナー半径",
"settings.components.corner_radius.description": "角張った端からカプセルのような形まで、共通のコーナー半径を調整し、内部のセーフエリアを拡張します。",
"settings.update.title": "アップデート",
"settings.update.current_version_label": "現在のバージョン",
"settings.update.latest_version_label": "最新リリース",
"settings.update.published_at_label": "公開日",
"settings.update.options_header": "アップデートオプション",
"settings.update.options_desc": "アップデートチェックとリリースチャンネルを設定します。",
"settings.update.auto_check_toggle": "起動時に自動的にアップデートを確認",
"settings.update.include_prerelease_toggle": "プレリリース版を含める",
"settings.update.channel_label": "アップデートチャンネル",
"settings.update.channel_stable": "安定版",
"settings.update.channel_preview": "プレビュー",
"settings.update.actions_header": "アップデートアクション",
"settings.update.actions_desc": "リリースを確認し、インストーラーをダウンロードし、アップデートを開始します。",
"settings.update.check_button": "アップデートを確認",
"settings.update.download_install_button": "ダウンロードしてインストール",
"settings.update.download_progress_idle": "ダウンロード進捗: -",
"settings.update.download_progress_format": "ダウンロード進捗: {0:F0}%",
"settings.update.status_ready": "アップデートを確認する準備ができました。",
"settings.update.status_channel_changed": "アップデートチャンネルが変更されました。再度確認してください。",
"settings.update.status_channel_changed_format": "アップデートチャンネルが{0}に切り替わりました。再度確認してください。",
"settings.update.status_windows_only": "自動インストーラーアップデートは現在Windowsでのみ利用可能です。",
"settings.update.status_checking": "GitHubリリースを確認中...",
"settings.update.status_check_failed_format": "アップデートの確認に失敗しました: {0}",
"settings.update.status_up_to_date": "最新バージョンを使用しています。",
"settings.update.status_asset_missing": "新しいリリースが利用可能ですが、互換性のあるインストーラーが見つかりませんでした。",
"settings.update.status_available_format": "新しいバージョン{0}が利用可能です。ダウンロードしてインストールをクリックしてください。",
"settings.update.status_downloading": "インストーラーをダウンロード中...",
"settings.update.status_download_failed_format": "ダウンロードに失敗しました: {0}",
"settings.update.status_launching_installer": "ダウンロード完了。インストーラーを起動中...",
"settings.update.status_installer_missing": "ダウンロード後にインストーラーファイルが見つかりませんでした。",
"settings.update.status_installer_started": "インストーラーが開始されました。アプリはアップデートのために終了します。",
"settings.update.status_elevation_cancelled": "管理者権限が付与されませんでした。アップデートはキャンセルされました。",
"settings.update.status_launch_failed_format": "インストーラーの起動に失敗しました: {0}",
"settings.about.title": "について",
"settings.about.version_format": "バージョン: {0}",
"settings.about.codename_format": "コードネーム: {0}",
"settings.about.font_format": "フォント: {0}",
"settings.about.startup_header": "Windowsのスタートアップ",
"settings.about.startup_desc": "Windowsへのサインイン時にアプリを自動的に起動します。",
"settings.about.startup_toggle": "Windowsサインイン時に起動",
"settings.about.render_mode_header": "アプリのレンダリングモード",
"settings.about.render_mode_desc": "レンダリングバックエンドを選択します。このオプションを変更した後、アプリを再起動します。サポートされていないモードはソフトウェアにフォールバックします。",
"settings.about.render_mode.default": "デフォルト",
"settings.about.render_mode.software": "ソフトウェア",
"settings.about.render_mode.angle_egl": "angleEgl",
"settings.about.render_mode.wgl": "WGL",
"settings.about.render_mode.vulkan": "Vulkan",
"settings.about.render_mode.unknown": "不明",
"settings.about.render_mode.current_label": "現在の実際のバックエンド",
"settings.about.render_mode.current_format": "現在のバックエンド: {0}",
"settings.about.render_mode.impl_format": "ランタイム実装: {0}",
"settings.about.render_mode.impl_unavailable": "ランタイム実装の詳細は利用できません。",
"settings.about.description": "アプリケーションの詳細。",
"settings.update.description": "リリースを確認し、アップデートチャンネルとダウンロードソースを選択し、アップデートのインストール方法を制御します。",
"settings.update.status_card_title": "アップデートステータス",
"settings.update.status_card_description": "アップデートを確認し、リリースの詳細を確認し、新しいバージョンが利用可能な場合はダウンロードまたはインストールを続行します。",
"settings.update.preferences_header": "アップデート設定",
"settings.update.preferences_description": "リリースチャンネル、インストーラーのダウンロードソース、インストール動作、ダウンロードの並列度を選択します。",
"settings.update.last_checked_label": "最終確認日時",
"settings.update.source_label": "ダウンロードソース",
"settings.update.source_github": "GitHub",
"settings.update.source_ghproxy": "gh-proxy",
"settings.update.source_github_desc": "GitHubからリリースアセットを直接ダウンロードします。",
"settings.update.source_ghproxy_desc": "GitHubリリースアセットをダウンロードする際にgh-proxyミラーを使用します。",
"settings.update.mode_label": "アップデートモード",
"settings.update.mode_manual": "手動アップデート",
"settings.update.mode_download_then_confirm": "サイレントダウンロード",
"settings.update.mode_silent_on_exit": "サイレントインストール",
"settings.update.mode_manual_desc": "アップデートの確認のみ。ダウンロードとインストールのタイミングを決定します。",
"settings.update.mode_download_then_confirm_desc": "バックグラウンドでアップデートをダウンロードし、インストール前に確認を求めます。",
"settings.update.mode_silent_on_exit_desc": "バックグラウンドでアップデートをダウンロードし、アプリの終了時にインストールします。",
"settings.update.channel_stable_desc": "安定ビルドは信頼性を重視し、ほとんどのユーザーにおすすめです。",
"settings.update.channel_preview_desc": "プレビュービルドは新しい機能が含まれる可能性がありますが、安定性が低い場合があります。",
"settings.update.download_threads_label": "ダウンロードスレッド",
"settings.update.download_threads_desc": "アプリケーションのアップデートパッケージの並列ダウンロードスレッド数を設定します。",
"settings.update.install_now_button": "今すぐインストール",
"settings.update.status_downloaded_confirm": "アップデートがダウンロードされました。確認してインストールのタイミングを選択してください。",
"settings.update.status_downloaded_exit": "アップデートがダウンロードされました。アプリの終了時にインストールされます。",
"settings.about.app_info_header": "アプリケーション情報",
"settings.about.update_header": "アップデート",
"settings.about.version_label": "バージョン",
"settings.about.codename_label": "コードネーム",
"settings.about.render_backend_label": "レンダーバックエンド",
"settings.about.render_backend_format": "レンダーバックエンド: {0}",
"settings.restart_dialog.title": "再起動が必要",
"settings.restart_dialog.render_mode_message": "レンダリングモードを「{0}」から「{1}」に切り替えるには、アプリを再起動します。今すぐ再起動しますか?",
"settings.restart_dialog.restart": "今すぐ再起動",
"settings.restart_dialog.later": "後で",
"settings.restart_dialog.cancel": "キャンセル",
"settings.restart_dock.title": "再起動が必要",
"settings.restart_dock.description": "一部の変更はアプリの再起動後に有効になります。",
"settings.restart_dock.button": "アプリを再起動",
"settings.footer": "LanMountainDesktop 設定",
"filepicker.title": "壁紙を選択",
"filepicker.image_files": "画像ファイル",
"filepicker.video_files": "動画ファイル",
"common.day": "昼",
"common.night": "夜",
"common.back": "戻る",
"common.close": "閉じる",
"common.unknown": "不明なエラー",
"common.recommended": "おすすめ",
"common.monet": "Monet",
"desktop.page_index_format": "デスクトップ {0}",
"launcher.title": "アプリランチャー",
"launcher.folder": "フォルダ",
"launcher.subtitle": "Windowsスタートメニューからのアプリとフォルダ",
"launcher.subtitle_linux": "Linuxデスクトップエントリから発見されたインストール済みアプリ",
"launcher.empty": "スタートメニューのエントリが見つかりません。",
"launcher.empty_linux": "Linuxデスクトップエントリが見つかりませんでした。",
"launcher.empty_folder": "このフォルダは空です。",
"launcher.folder_items_format": "{0}個のアプリ",
"launcher.context.hide_icon": "アイコンを非表示",
"launcher.action.hide": "非表示",
"settings.launcher.title": "アプリランチャー",
"settings.launcher.description": "アプリランチャーの非表示アプリとフォルダを管理します。",
"settings.launcher.hidden_header": "非表示アイテム",
"settings.launcher.hidden_desc": "非表示のランチャーエントリを確認し、再度表示します。",
"settings.launcher.hidden_hint": "デスクトップ編集モードで、ランチャーアイコンを選択して非表示をクリックします。非表示のエントリはここに表示されます。",
"settings.launcher.hidden_empty": "非表示アイテムはありません。",
"settings.launcher.hidden_summary_format": "{0}個の非表示アイテム",
"settings.launcher.hidden_type_folder": "フォルダ",
"settings.launcher.hidden_type_shortcut": "アプリ",
"settings.launcher.restore_button": "再表示",
"settings.plugins.title": "プラグイン",
"settings.plugins.runtime_header": "プラグインランタイム",
"settings.plugins.runtime_desc": "プラグインランタイムの状態とロード結果を確認します。",
"settings.plugins.runtime_hint": "このページには、インストールされたプラグインの発見ステータス、ロード結果、ランタイム診断が表示されます。",
"settings.plugins.runtime_status": "プラグインの発見が完了すると、プラグインランタイムのステータスがここに表示されます。",
"settings.plugins.description": "インストールされたプラグインを管理し、ランタイムの状態を確認します。",
"settings.plugins.initial_status": "プラグインの状態を更新して、最新のインストール済みプラグインを確認してください。",
"settings.plugins.refresh_button": "プラグインを更新",
"settings.plugins.refresh_success_installed_format": "{0}個のインストール済みプラグインをロードしました。",
"settings.plugins.refresh_success_format": "{0}個のインストール済みプラグインと{1}個のマーケットプレイスエントリをロードしました。",
"settings.plugins.refresh_failed": "プラグインマーケットインデックスのロードに失敗しました。",
"settings.plugins.marketplace_header": "マーケットプレイス",
"settings.plugins.marketplace_empty": "現在、マーケットプレイスのプラグインはありません。",
"settings.plugins.delete_button_short": "削除",
"settings.plugins.install_button_short": "インストール",
"settings.plugins.restart_required": "プラグインの変更は再起動後に有効になります。",
"settings.plugins.toggle_unchanged_format": "プラグイン「{0}」は変更されませんでした。",
"settings.plugins.delete_failed_name_format": "プラグイン「{0}」の削除に失敗しました。",
"settings.plugins.install_failed_name_format": "「{0}」のインストールに失敗しました。",
"settings.plugins.installed_header": "インストール済みプラグイン",
"settings.plugins.installed_desc": "インストール済みプラグインを確認し、ここで削除します。",
"settings.plugins.import_header": "パッケージからインストール",
"settings.plugins.import_desc": ".laappパッケージを開き、ローカルプラグインディレクトリにステージングします。",
"settings.plugins.restart_hint": "プラグインのインストールと削除の変更は、アプリの再起動後に有効になります。",
"settings.plugins.empty": "プラグインが見つかりません。",
"settings.plugins.runtime_unavailable": "プラグインランタイムは利用できません。",
"settings.plugins.summary_format": "{0}個のプラグインを検出; 有効 {1}; ロード済み {2}; 設定ページ {3}; ウィジェット {4}; 失敗 {5}。",
"settings.plugins.summary_item_format": "{0} v{1} | {2}",
"settings.plugins.state.enabled": "有効",
"settings.plugins.state.enabled_failed": "有効 / ロード失敗",
"settings.plugins.state.disabled": "無効",
"settings.plugins.state.loaded": "ロード済み",
"settings.plugins.state.load_failed": "ロード失敗",
"settings.plugins.toggle_on": "有効",
"settings.plugins.toggle_off": "無効",
"settings.plugins.toggle_result_format": "プラグイン「{0}」は次回起動時に{1}になりました。ページとウィジェットの変更を適用するには、アプリを再起動してください。",
"settings.plugins.toggle_state_enabled": "有効",
"settings.plugins.toggle_state_disabled": "無効",
"settings.plugins.toggle_failed_detail_format": "プラグイン「{0}」の更新に失敗しました: {1}",
"settings.plugins.install_button": ".laappパッケージを開く",
"settings.plugins.install_unavailable": "プラグインランタイムが利用できないため、.laappパッケージをインストールできません。",
"settings.plugins.install_hint_format": ".laappパッケージを開いて次にインストールします: {0}",
"settings.plugins.install_picker_title": "プラグインパッケージを選択",
"settings.plugins.install_file_type": ".laappプラグインパッケージ",
"settings.plugins.install_picker_unavailable": "ストレージプロバイダが利用できません。",
"settings.plugins.install_copy_failed": "選択した.laappパッケージのコピーに失敗しました。",
"settings.plugins.install_success_format": "プラグイン「{0}」がインストールされました。新しく追加された設定ページとウィジェットを適用するには、アプリを再起動してください。",
"settings.plugins.install_failed_format": "プラグインパッケージのインストールに失敗しました: {0}",
"settings.plugins.delete_button": "プラグインを削除",
"settings.plugins.delete_success_format": "プラグイン「{0}」は削除のためにステージングされました。削除を完了するには、アプリを再起動してください。",
"settings.plugins.delete_failed_format": "プラグインの削除に失敗しました: {0}",
"settings.plugins.delete_failed_detail_format": "プラグイン「{0}」の削除に失敗しました: {1}",
"settings.plugins.publisher_format": "パブリッシャー: {0}",
"settings.plugins.publisher_unknown": "不明なパブリッシャー",
"settings.plugins.source_package": ".laappパッケージ",
"settings.plugins.source_manifest": "ルーズマニフェスト",
"settings.plugins.subtitle_format": "{0} | {1} | {2}",
"settings.plugins.detail_format": "設定ページ: {0} | ウィジェット: {1}",
"settings.nav.plugin_market": "プラグインマーケット",
"settings.plugin_market.title": "プラグインマーケット",
"settings.plugin_market.subtitle": "公式LanAirAppソースからプラグインを参照し、インストールをステージングします。",
"settings.plugin_market.unavailable": "プラグインランタイムが利用できないため、公式マーケットを開けません。",
"settings.update.status_idle": "アップデートの確認はまだ実行されていません。",
"settings.update.status_preferences_saved": "アップデート設定が保存されました。",
"settings.update.status_check_failed": "アップデートの確認に失敗しました。",
"settings.update.status_available_summary_format": "アップデートあり: {0}(現在: {1}",
"settings.update.status_up_to_date_format": "最新版です({0})。",
"settings.window.drawer_default": "詳細",
"market.toolbar.search_placeholder": "プラグインを検索",
"market.toolbar.refresh": "更新",
"market.status.loading": "公式プラグインマーケットをロード中...",
"market.status.loaded_network_format": "公式ソースから{0}個のプラグインをロードしました。",
"market.status.loaded_cache_format": "公式ソースが利用できません。キャッシュから{0}個のプラグインをロードしました。理由: {1}",
"market.status.load_failed_format": "プラグインマーケットのロードに失敗しました: {0}",
"market.status.installing_format": "プラグイン「{0}」をダウンロードしてステージング中...",
"market.status.install_success_format": "プラグイン「{0}」がステージングされました。適用するにはアプリを再起動してください。",
"market.status.install_failed_format": "プラグインのインストールに失敗しました: {0}",
"market.status.host_incompatible_format": "このホストは古すぎます。バージョン{0}以降が必要です。",
"market.list.empty": "プラグインマーケットはまだロードされていません。",
"market.list.no_results": "現在の検索に一致するプラグインはありません。",
"market.card.subtitle_format": "{0} | v{1}",
"market.card.loaded": "ロード済み",
"market.card.pending_restart": "再起動が必要",
"market.detail.placeholder": "左側のプラグインを選択して詳細を確認します。",
"market.detail.author": "パブリッシャー",
"market.detail.version": "バージョン",
"market.detail.api_version": "APIバージョン",
"market.detail.min_host_version": "最小ホストバージョン",
"market.detail.installed_version": "インストール済みバージョン",
"market.detail.not_installed": "未インストール",
"market.detail.readme": "README",
"market.detail.plugin_information": "プラグイン情報",
"market.detail.author_subtitle_format": "{0}作成",
"market.detail.package_size": "パッケージサイズ",
"market.detail.published_at": "公開日",
"market.detail.updated_at": "更新日",
"market.detail.tags": "タグ",
"market.detail.project": "プロジェクト",
"market.detail.state": "インストール状態",
"market.detail.market_source": "マーケットソース",
"market.detail.homepage": "ホームページ",
"market.detail.repository": "リポジトリ",
"market.detail.release_notes": "リリースノート",
"market.detail.dependencies": "依存関係",
"market.detail.dependencies_empty": "このプラグインは共有コントラクトの依存関係を宣言していません。",
"market.detail.readme_loading": "READMEをロード中...",
"market.detail.readme_empty": "READMEは空です。",
"market.detail.readme_error_format": "READMEをロードできませんでした: {0}",
"market.detail.state.not_installed": "未インストール",
"market.detail.state.update_available": "アップデートあり",
"market.detail.state.installed": "インストール済み",
"market.detail.unknown": "不明",
"market.button.install": "インストール",
"market.button.update": "アップデート",
"market.button.installed": "インストール済み",
"market.button.installing": "インストール中...",
"market.button.restart": "再起動して適用",
"button.component_library": "デスクトップを編集",
"tooltip.component_library": "デスクトップを編集",
"component_library.title": "ウィジェット",
"component_library.empty": "スワイプしてカテゴリを選択し、タップして開き、ウィジェットをデスクトップにドラッグします。",
"component_library.drag_hint": "ドラッグして配置",
"component.delete": "削除",
"component.edit": "編集",
"component.editor.instance_scope": "変更はこのコンポーネントインスタンスにのみ適用されます。",
"component.editor.info_header": "コンポーネント情報",
"component.editor.id_label": "コンポーネントID",
"component.editor.placement_label": "配置ID",
"component.editor.scope_label": "スコープ",
"component.editor.scope_instance": "インスタンススコープのエディタ",
"component_category.clock": "時計",
"component_category.date": "カレンダー",
"component_category.weather": "天気",
"component_category.board": "ボード",
"component_category.media": "メディア",
"component_category.info": "情報",
"component_category.calculator": "計算機",
"component_category.study": "学習",
"component_category.file": "ファイル",
"component.date": "カレンダー",
"component.month_calendar": "月間カレンダー",
"component.lunar_calendar": "旧暦カレンダー",
"component.desktop_clock": "時計",
"component.weather_clock": "天気時計",
"component.world_clock": "世界時計",
"component.desktop_timer": "タイマー",
"component.desktop_weather": "天気",
"component.hourly_weather": "時間別天気",
"component.multiday_weather": "数日間天気",
"component.extended_weather": "拡張天気",
"component.class_schedule": "時間割",
"component.music_control": "音楽コントロール",
"component.audio_recorder": "レコーダー",
"component.daily_poetry": "今日の詩",
"component.daily_artwork": "今日のアート",
"component.daily_word": "今日の言葉",
"component.daily_word_2x2": "今日の言葉 2x2",
"component.cnr_daily_news": "CNRヘッドライン",
"component.ifeng_news": "iFengニュース",
"component.bilibili_hot_search": "Bilibiliトレンド",
"component.baidu_hot_search": "Baiduトレンド",
"component.stcn24_forum": "STCN 24",
"component.exchange_rate_converter": "為替レート変換",
"component.whiteboard": "黒板(縦向き)",
"component.blackboard_landscape": "黒板(横向き)",
"component.browser": "ブラウザ",
"component.office_recent_documents": "最近のドキュメント",
"whiteboard.settings.desc": "各黒板は独自のノート履歴を保持し、独立して保存します。",
"whiteboard.settings.retention.title": "ノートの保持期間",
"whiteboard.settings.retention.desc": "この黒板が保存されたノートを保持する期間を選択します。期限切れのデータは自動的に削除されます。",
"whiteboard.settings.retention.option": "{0}日",
"whiteboard.settings.instance_scope": "この保持設定は黒板コンポーネントインスタンスごとに保存されます。",
"office_recent_documents.settings.desc": "このウィジェットが最近のドキュメントをスキャンするWindowsとOfficeのソースを選択します。",
"office_recent_documents.settings.sources_title": "最近のドキュメントソース",
"office_recent_documents.settings.sources_desc": "複数のソースを組み合わせることができます。レジストリ選択は、Office相互運用MRUフォールバックも利用可能にします。",
"office_recent_documents.settings.source.registry": "OfficeレジストリMRU",
"office_recent_documents.settings.source.recent_folders": "Windowsの最近使ったフォルダ",
"office_recent_documents.settings.source.jump_lists": "Windowsジャンプリスト",
"office_recent_documents.settings.hint": "すべてのソースを無効にすると、少なくとも1つのソースが再度有効になるまで、このウィジェットは空のままになります。",
"component.removable_storage": "リムーバブルストレージ",
"component.holiday_calendar": "祝日カレンダー",
"component.study_environment": "環境",
"component.study_session_control": "学習セッション制御",
"component.study_session_history": "セッション履歴",
"component.study_noise_curve": "ノイズカーブ",
"component.study_noise_distribution": "ノイズ分布",
"component.study_score_overview": "学習スコア概要",
"component.study_deduction_reasons": "減点理由",
"component.study_interrupt_density": "中断密度",
"desktop_clock.settings.title": "時計の設定",
"desktop_clock.settings.desc": "単一時計のタイムゾーンを選択します。",
"desktop_clock.settings.timezone_label": "タイムゾーン",
"desktop_clock.settings.second_mode_label": "秒針",
"clock.second_mode.tick": "ティック",
"clock.second_mode.sweep": "スイープ",
"poetry.widget.loading_content": "詩を読み込み中...",
"poetry.widget.loading_author": "読み込み中...",
"poetry.widget.fetch_failed": "詩の取得に失敗しました",
"poetry.widget.fallback_content": "今日の詩は一時的に利用できません。",
"poetry.widget.fallback_author": "後でもう一度お試しください",
"poetry.widget.unknown_author": "不明",
"artwork.widget.loading": "読み込み中...",
"artwork.widget.loading_title": "今日のアート",
"artwork.widget.loading_subtitle": "今日の傑作を取得中",
"artwork.widget.fetch_failed": "アートの取得に失敗しました",
"artwork.widget.fallback_title": "今日のアート",
"artwork.widget.fallback_artist": "おすすめサービスは利用できません",
"artwork.widget.fallback_year": "後でもう一度お試しください",
"artwork.widget.unknown_artist": "不明なアーティスト",
"dailyword.widget.loading": "読み込み中...",
"dailyword.widget.loading_word": "今日の言葉",
"dailyword.widget.loading_pronunciation": "発音を取得中...",
"dailyword.widget.loading_meaning": "意味を取得中...",
"dailyword.widget.loading_example": "例文を取得中...",
"dailyword.widget.loading_example_translation": "読み込み中...",
"dailyword.widget.fetch_failed": "今日の言葉の取得に失敗しました",
"dailyword.widget.fallback_word": "今日の言葉",
"dailyword.widget.fallback_pronunciation": "発音は利用できません",
"dailyword.widget.fallback_meaning": "Youdao辞書は一時的に利用できません。",
"dailyword.widget.fallback_example": "更新ボタンをタップして再試行してください。",
"dailyword.widget.fallback_example_translation": "ネットワークが回復すると再試行します。",
"dailyword2x2.widget.tap_to_show": "タップして意味を表示",
"cnrnews.widget.loading": "読み込み中...",
"cnrnews.widget.loading_title": "CNRヘッドラインを取得中",
"cnrnews.widget.loading_subtitle": "お待ちください",
"cnrnews.widget.fetch_failed": "ニュースの取得に失敗しました",
"cnrnews.widget.fallback_title": "CNRニュースは一時的に利用できません",
"cnrnews.widget.fallback_subtitle": "更新をタップして再試行してください",
"cnrnews.widget.hot_label": "ホット",
"bilihot.widget.brand": "Bilibiliトレンド",
"bilihot.widget.top_right_label": "Bilibiliトレンド",
"bilihot.widget.search_entry": "検索",
"bilihot.widget.search_placeholder": "トレンドトピックを検索",
"bilihot.widget.loading": "読み込み中...",
"bilihot.widget.loading_item": "読み込み中...",
"bilihot.widget.fetch_failed": "トレンドの取得に失敗しました",
"bilihot.widget.fallback_item": "トレンドデータなし",
"bilihot.widget.more_hot": "もっとトレンドを見る",
"baiduhot.widget.brand": "Baiduトレンド",
"baiduhot.widget.loading": "読み込み中...",
"baiduhot.widget.loading_item": "読み込み中...",
"baiduhot.widget.fetch_failed": "トレンドの取得に失敗しました",
"baiduhot.widget.fallback_item": "トレンドデータなし",
"baiduhot.widget.refresh_tooltip": "更新",
"ifeng.widget.brand": "iFengニュース",
"ifeng.widget.loading": "読み込み中...",
"ifeng.widget.loading_item": "読み込み中...",
"ifeng.widget.fetch_failed": "ニュースの取得に失敗しました",
"ifeng.widget.fallback_item": "ニュースデータなし",
"ifeng.widget.refresh_tooltip": "更新",
"dailyword.settings.title": "今日の言葉の設定",
"dailyword.settings.desc": "自動更新と更新間隔を設定します。",
"dailyword.settings.auto_refresh_label": "自動更新",
"dailyword.settings.auto_refresh_enabled": "自動更新を有効にする",
"dailyword.settings.frequency_label": "更新間隔",
"bilihot.settings.title": "Bilibiliトレンドの設定",
"bilihot.settings.desc": "自動更新と更新間隔を設定します。",
"bilihot.settings.auto_refresh_label": "自動更新",
"bilihot.settings.auto_refresh_enabled": "自動更新を有効にする",
"bilihot.settings.frequency_label": "更新間隔",
"baiduhot.settings.title": "Baiduトレンドの設定",
"baiduhot.settings.desc": "ソース、自動更新、更新間隔を設定します。",
"baiduhot.settings.source_label": "データソース",
"baiduhot.settings.source_official": "公式ソース",
"baiduhot.settings.source_rss": "サードパーティRSS",
"baiduhot.settings.auto_refresh_label": "自動更新",
"baiduhot.settings.auto_refresh_enabled": "自動更新を有効にする",
"baiduhot.settings.frequency_label": "更新間隔",
"ifeng.settings.title": "iFengニュースの設定",
"ifeng.settings.desc": "チャンネル、自動更新、更新間隔を設定します。",
"ifeng.settings.channel_label": "ニュースチャンネル",
"ifeng.settings.channel_comprehensive": "総合",
"ifeng.settings.channel_mainland": "中国本土",
"ifeng.settings.channel_taiwan": "台湾",
"ifeng.settings.auto_refresh_label": "自動更新",
"ifeng.settings.auto_refresh_enabled": "自動更新を有効にする",
"ifeng.settings.frequency_label": "更新間隔",
"refresh.frequency.5m": "5分",
"refresh.frequency.10m": "10分",
"refresh.frequency.12m": "12分",
"refresh.frequency.15m": "15分",
"refresh.frequency.20m": "20分",
"refresh.frequency.30m": "30分",
"refresh.frequency.40m": "40分",
"refresh.frequency.1h": "1時間",
"refresh.frequency.3h": "3時間",
"refresh.frequency.6h": "6時間",
"refresh.frequency.12h": "12時間",
"refresh.frequency.24h": "24時間",
"weather.widget.settings.title": "天気ウィジェットの設定",
"weather.widget.settings.desc": "すべての天気ウィジェットの自動更新と更新間隔を設定します。",
"weather.widget.settings.auto_refresh_label": "自動更新",
"weather.widget.settings.auto_refresh_enabled": "自動更新を有効にする",
"weather.widget.settings.frequency_label": "更新間隔",
"weather.widget.settings.frequency_10m": "10分",
"weather.widget.settings.frequency_12m": "12分",
"weather.widget.settings.frequency_15m": "15分",
"weather.widget.settings.frequency_30m": "30分",
"weather.widget.settings.frequency_1h": "1時間",
"weather.widget.settings.frequency_3h": "3時間",
"stcn24.widget.loading": "読み込み中...",
"stcn24.widget.loading_item": "読み込み中...",
"stcn24.widget.fetch_failed": "フォーラム投稿の取得に失敗しました",
"stcn24.widget.fallback_item": "投稿なし",
"stcn24.settings.title": "STCN 24の設定",
"stcn24.settings.desc": "情報ソース、自動更新、更新間隔を設定します。",
"stcn24.settings.source_label": "情報ソース",
"stcn24.settings.source_latest_created": "最新の投稿",
"stcn24.settings.source_latest_activity": "最新のアクティビティ",
"stcn24.settings.source_most_replies": "返信数順",
"stcn24.settings.source_earliest_created": "最古の投稿",
"stcn24.settings.source_earliest_activity": "最古のアクティビティ",
"stcn24.settings.source_least_replies": "返信が少ない順",
"stcn24.settings.source_frontpage_latest": "フロントページ最新",
"stcn24.settings.source_frontpage_earliest": "フロントページ最古",
"stcn24.settings.auto_refresh_label": "自動更新",
"stcn24.settings.auto_refresh_enabled": "自動更新を有効にする",
"stcn24.settings.frequency_label": "更新間隔",
"stcn24.settings.frequency_5m": "5分",
"stcn24.settings.frequency_10m": "10分",
"stcn24.settings.frequency_20m": "20分",
"stcn24.settings.frequency_30m": "30分",
"stcn24.settings.frequency_1h": "1時間",
"stcn24.settings.frequency_3h": "3時間",
"exchange.widget.loading": "為替レートを読み込み中...",
"exchange.widget.fetch_failed": "為替レートの取得に失敗しました",
"cnrnews.settings.title": "CNRの設定",
"cnrnews.settings.desc": "自動ローテーションと更新間隔を設定します。",
"cnrnews.settings.auto_rotate_label": "自動ローテーション",
"cnrnews.settings.auto_rotate_enabled": "自動ローテーションを有効にする",
"cnrnews.settings.frequency_label": "ローテーション間隔",
"cnrnews.settings.frequency_5m": "5分",
"cnrnews.settings.frequency_10m": "10分",
"cnrnews.settings.frequency_40m": "40分",
"cnrnews.settings.frequency_1h": "1時間",
"cnrnews.settings.frequency_12h": "12時間",
"cnrnews.settings.frequency_24h": "24時間",
"artwork.settings.title": "今日のアートの設定",
"artwork.settings.desc": "今日のアートで使用されるデータソースを切り替えます。",
"artwork.settings.source_label": "ミラーソース",
"artwork.settings.source_domestic": "国内ミラー",
"artwork.settings.source_overseas": "海外ミラー",
"artwork.settings.source_status_domestic": "現在のソース: 国内ミラー(中国ネットワーク向けに最適化)",
"artwork.settings.source_status_overseas": "現在のソース: 海外ミラー(美術館のおすすめ)",
"music.widget.unsupported": "このプラットフォームでは音楽コントロールはサポートされていません",
"music.widget.unsupported_hint": "このウィジェットにはWindows SMTCが必要です",
"music.widget.no_session": "音楽ソースなし",
"music.widget.no_session_hint": "アプリストアからQQ音楽/酷狗/網易雲音楽をインストールしてください",
"music.widget.open_player": "プレーヤーを開く",
"music.widget.unknown_title": "不明なタイトル",
"music.widget.unknown_artist": "不明なアーティスト",
"music.widget.status.opened": "開かれました",
"music.widget.status.changing": "変更中",
"music.widget.status.stopped": "停止",
"music.widget.status.playing": "再生中",
"music.widget.status.paused": "一時停止",
"recording.widget.title": "レコーダー",
"recording.widget.hint.ready": "赤いボタンをタップして録音",
"recording.widget.hint.recording": "録音中",
"recording.widget.hint.paused": "一時停止",
"recording.widget.hint.unsupported": "マイクが利用できません",
"recording.widget.hint.error": "録音に失敗しました",
"recording.widget.hint.saved_format": "保存しました {0}",
"recording.widget.save_picker_title": "録音ファイルを保存",
"recording.widget.save_picker_type": "WAVオーディオ",
"study.environment.status_label": "環境",
"study.environment.status.initializing": "初期化中",
"study.environment.status.ready": "準備完了",
"study.environment.status.quiet": "静か",
"study.environment.status.noisy": "うるさい",
"study.environment.status.paused": "一時停止",
"study.environment.status.error": "エラー",
"study.environment.status.unsupported": "未対応",
"study.environment.value.unavailable": "--",
"study.environment.value.display_format": "{0:F1} dB",
"study.environment.value.dbfs_format": "{0:F1} dBFS",
"study.environment.settings.title": "環境ウィジェットの設定",
"study.environment.settings.desc": "右側のリアルタイムノイズ値表示を設定します。",
"study.environment.settings.show_display_db": "表示dBを表示",
"study.environment.settings.show_dbfs": "dBFSを表示",
"study.environment.settings.hint": "少なくとも1つの表示モードを有効にしておく必要があります。",
"removable_storage.settings.desc": "接続されたUSBドライブを表示し、クイックオープンと取り出しアクションを提供します。",
"removable_storage.settings.behavior_title": "動作",
"removable_storage.settings.behavior_desc": "ウィジェットはリムーバブルドライブを自動的に監視し、最新の挿入されたUSBドライブに切り替わります。",
"removable_storage.action.open": "開く",
"removable_storage.action.eject": "取り出し",
"removable_storage.widget.default_name": "リムーバブルドライブ",
"removable_storage.widget.empty_title": "デバイスが挿入されていません",
"removable_storage.widget.empty_subtitle": "USBドライブを挿入してここに表示します。",
"removable_storage.widget.empty_hint": "リムーバブルデバイスが挿入されるまで、ボタンは無効のままです。",
"removable_storage.widget.ready": "開くか取り出す準備ができました。",
"removable_storage.widget.ejecting": "ドライブを取り出し中...",
"removable_storage.widget.eject_failed": "このドライブを取り出せませんでした。上のファイルを閉じて再試行してください。",
"removable_storage.widget.open_failed": "このドライブを開けませんでした。",
"removable_storage.widget.refresh_failed": "ドライブリストの更新に失敗しました。",
"study.session_control.action.start": "学習セッションを開始",
"study.session_control.action.stop": "学習セッションを停止",
"study.session_control.idle_hint": "右のボタンをタップして開始",
"study.session_control.report_preview": "レポートをプレビュー",
"study.session_control.report_confirm_hint": "右のボタンをタップして確認",
"study.session_control.running_elapsed_format": "経過 {0}",
"study.session_control.last_session_format": "前回 {0}",
"study.session_control.start_failed": "セッションを開始できません",
"study.session_control.stop_failed": "セッションを停止できません",
"study.session_history.title": "セッション履歴",
"study.session_history.empty": "セッション履歴なし",
"study.session_history.select_failed": "セッションを切り替えられません",
"study.session_history.rename_failed": "セッション名を変更できません",
"study.session_history.delete_failed": "セッションを削除できません",
"study.session_history.rename_placeholder": "セッション名を入力",
"study.session_history.rename_confirm": "名前変更を確認",
"study.session_history.rename_cancel": "名前変更をキャンセル",
"study.session_history.loading": "データを読み込み中...",
"study.session_history.loaded": "データが読み込まれました",
"study.session_history.duration_format": "{0:hh\\:mm\\:ss}",
"study.session_history.meta_format": "{0} · 平均 {1:F1}",
"study.session_history.action.view": "表示",
"study.session_history.action.rename": "名前変更",
"study.session_history.action.delete": "削除",
"study.session_history.dialog.rename_title": "セッション名を変更",
"study.session_history.dialog.rename_message": "「{0}」の新しい名前を入力してください。",
"study.session_history.dialog.delete_title": "セッションを削除",
"study.session_history.dialog.delete_message": "「{0}」を削除しますか?これは元に戻せません。",
"study.session_history.dialog.delete_confirm": "削除",
"study.noise_curve.value_format": "{0:F1} dB",
"study.noise_curve.axis.now": "現在",
"study.noise_distribution.title": "ノイズレベル分布",
"study.noise_distribution.mode.realtime": "リアルタイム",
"study.noise_distribution.mode.session": "セッション",
"study.noise_distribution.summary.mainly_format": "主に: {0}",
"study.noise_distribution.summary.latest_format": "最新: {0}",
"study.noise_distribution.summary.compact_format": "主 {0} · 新 {1}",
"study.noise_distribution.level.quiet": "静か",
"study.noise_distribution.level.normal": "普通",
"study.noise_distribution.level.noisy": "うるさい",
"study.noise_distribution.level.extreme": "極端",
"study.noise_distribution.axis.extreme": "極端",
"study.noise_distribution.axis.noisy": "うるさい",
"study.noise_distribution.axis.normal": "普通",
"study.noise_distribution.axis.quiet": "静か",
"study.noise_distribution.axis.now": "現在",
"study.score_overview.title": "学習スコア",
"study.score_overview.mode.realtime": "リアルタイム",
"study.score_overview.mode.session": "セッション",
"study.score_overview.current": "現在",
"study.score_overview.average": "平均",
"study.score_overview.minimum": "最小",
"study.score_overview.maximum": "最大",
"study.score_overview.average_short": "平均",
"study.score_overview.minimum_short": "最小",
"study.score_overview.maximum_short": "最大",
"study.score_overview.unavailable": "--",
"study.deduction.title": "減点理由",
"study.deduction.mode.realtime": "リアルタイム",
"study.deduction.mode.session": "セッション",
"study.deduction.reason.sustained": "持続ノイズ",
"study.deduction.reason.time": "閾値超過時間",
"study.deduction.reason.segment": "中断頻度",
"study.deduction.reason.sustained_short": "持続",
"study.deduction.reason.time_short": "時間",
"study.deduction.reason.segment_short": "中断",
"study.deduction.metric.sustained_format": "p50 {0:F1} dBFS",
"study.deduction.metric.sustained_short_format": "p50 {0:F1}",
"study.deduction.metric.time_format": "{0:F1}%超過",
"study.deduction.metric.time_short_format": "{0:F1}%",
"study.deduction.metric.segment_format": "{0:F1}/分",
"study.deduction.metric.segment_short_format": "{0:F1}/分",
"study.deduction.loss_format": "-{0:F1}",
"study.deduction.total_loss_format": "合計 -{0:F1}",
"study.deduction.total_score_format": "スコア {0:F1}",
"study.deduction.total_loss_unavailable": "合計 {0}",
"study.deduction.total_score_unavailable": "スコア {0}",
"study.deduction.unavailable": "--",
"study.interrupt_density.title": "中断密度",
"study.interrupt_density.mode.realtime": "リアルタイム",
"study.interrupt_density.mode.session": "セッション",
"study.interrupt_density.unit": "/分",
"study.interrupt_density.segment_count": "中断回数",
"study.interrupt_density.segment_count_short": "回数",
"study.interrupt_density.duration": "期間",
"study.interrupt_density.duration_short": "時間",
"study.interrupt_density.density_value_format": "{0:F1}",
"study.interrupt_density.segment_count_value_format": "{0}",
"study.interrupt_density.level_format": "レベル {0}",
"study.interrupt_density.level.calm": "穏やか",
"study.interrupt_density.level.normal": "普通",
"study.interrupt_density.level.frequent": "頻繁",
"study.interrupt_density.level.severe": "深刻",
"study.interrupt_density.threshold_format": "ペナルティ閾値 {0:F1}/分",
"study.interrupt_density.unavailable": "--",
"desktop.add_page": "ページを追加",
"desktop.delete_page": "ページを削除",
"placement.fill": "フィル",
"placement.fit": "フィット",
"placement.stretch": "ストレッチ",
"placement.center": "中央",
"placement.tile": "タイル",
"single_instance.notice.title": "アプリは既に実行中",
"single_instance.notice.description": "アプリは既に実行中です。複数回クリックして開く必要はありません。",
"single_instance.notice.button": "OK",
"market.status.install_success_restart_format": "✓ プラグイン「{0}」が正常にインストールされました!有効にするには、アプリケーションを再起動してください。",
"market.dialog.restart_message_format": "プラグイン「{0}」が正常にインストールされました。\n\nこのプラグインを使用するには、今すぐアプリケーションを再起動する必要があります。\n\n再起動しますか",
"component.settings.color_scheme": "カラースキーム"
}

View File

@@ -41,6 +41,23 @@
"settings.wallpaper.type_label": "壁纸类型",
"settings.wallpaper.type.image": "图片",
"settings.wallpaper.type.solid_color": "纯色",
"settings.wallpaper.type.system": "系统壁纸",
"settings.wallpaper.system.label": "系统壁纸",
"settings.wallpaper.system.unavailable": "无法读取系统壁纸",
"settings.wallpaper.refresh_interval": "刷新频率",
"settings.wallpaper.refresh_now": "立即刷新",
"settings.wallpaper.refresh.30s": "30 秒",
"settings.wallpaper.refresh.1m": "1 分钟",
"settings.wallpaper.refresh.5m": "5 分钟",
"settings.wallpaper.refresh.10m": "10 分钟",
"settings.wallpaper.refresh.15m": "15 分钟",
"settings.wallpaper.refresh.30m": "30 分钟",
"settings.wallpaper.refresh.1h": "1 小时",
"settings.wallpaper.refresh.2h": "2 小时",
"settings.wallpaper.refresh.4h": "4 小时",
"settings.wallpaper.refresh.8h": "8 小时",
"settings.wallpaper.refresh.12h": "12 小时",
"settings.wallpaper.refresh.24h": "24 小时",
"settings.wallpaper.color_label": "壁纸颜色",
"settings.wallpaper.custom_color_tooltip": "自定义颜色",
"settings.wallpaper.custom_color_apply": "应用",
@@ -216,7 +233,14 @@
"schedule.settings.unnamed": "未命名课表",
"schedule.settings.delete": "删除",
"schedule.settings.picker_title": "选择 ClassIsland 课表文件",
"schedule.settings.picker_file_type": "ClassIsland CSES 课表",
"schedule.settings.picker_file_type.all": "ClassIsland 课表文件",
"schedule.settings.picker_file_type.json": "ClassIsland 档案 (JSON)",
"schedule.settings.picker_file_type.cses": "CSES 课表 (YAML)",
"schedule.settings.semester.title": "学期设置",
"schedule.settings.semester.start_date": "学期开始日期",
"schedule.settings.semester.week_cycle": "周循环",
"schedule.settings.semester.week_cycle_desc": "设置多周课表轮换周期,用于计算当前是第几周。",
"schedule.settings.semester.week_cycle_format": "{0} 周轮换",
"worldclock.settings.title": "世界时钟设置",
"worldclock.settings.desc": "分别为四个时钟选择时区。",
"worldclock.settings.clock_1": "时钟 1",
@@ -247,6 +271,7 @@
"settings.region.language_label": "语言",
"settings.region.language_zh": "中文",
"settings.region.language_en": "英文",
"settings.region.language_ja": "日文",
"settings.region.timezone_header": "时区",
"settings.region.timezone_desc": "选择时区。时钟与日历组件会使用该时区。",
"settings.region.applied_format": "语言已切换为:{0}",

View File

@@ -33,6 +33,8 @@ public sealed class AppSettingsSnapshot
public string WallpaperPlacement { get; set; } = "Fill";
public int SystemWallpaperRefreshIntervalSeconds { get; set; } = 300;
public int SettingsTabIndex { get; set; } = 0;
public string? SettingsTabTag { get; set; }

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
namespace LanMountainDesktop.Models;
@@ -12,6 +13,10 @@ public sealed class ComponentSettingsSnapshot
public string ActiveImportedClassScheduleId { get; set; } = string.Empty;
public DateOnly? SemesterStartDate { get; set; }
public int SemesterWeekCycle { get; set; } = 1;
public bool StudyEnvironmentShowDisplayDb { get; set; } = true;
public bool StudyEnvironmentShowDbfs { get; set; }

View File

@@ -11,7 +11,7 @@ using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop;
sealed class Program
public sealed class Program
{
internal static string StartupRenderMode { get; private set; } = AppRenderingModeHelper.Default;
@@ -67,7 +67,12 @@ sealed class Program
}
}
public static AppBuilder BuildAvaloniaApp(string renderMode = AppRenderingModeHelper.Default)
public static AppBuilder BuildAvaloniaApp()
{
return BuildAvaloniaApp(AppRenderingModeHelper.Default);
}
public static AppBuilder BuildAvaloniaApp(string renderMode)
{
var builder = AppBuilder.Configure<App>()
.UsePlatformDetect()

View File

@@ -12,7 +12,7 @@ namespace LanMountainDesktop.Services;
public interface IClassIslandScheduleDataService
{
ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null);
ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null, DateOnly? semesterStartDate = null, int semesterWeekCycle = 1);
bool TryResolveClassPlanForDate(
ClassIslandScheduleSnapshot snapshot,
@@ -43,7 +43,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
.IgnoreUnmatchedProperties()
.Build();
public ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null)
public ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null, DateOnly? semesterStartDate = null, int semesterWeekCycle = 1)
{
var warnings = new List<string>();
try
@@ -73,11 +73,11 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
ClassIslandScheduleSnapshot snapshot;
if (source.SourceKind == ScheduleSourceKind.Cses)
{
snapshot = ParseCsesSnapshot(source);
snapshot = ParseCsesSnapshot(source, semesterStartDate, semesterWeekCycle);
}
else
{
var cycleRule = ParseCycleRule(source.SettingsPath, warnings);
var cycleRule = ParseCycleRule(source.SettingsPath, warnings, semesterStartDate, semesterWeekCycle);
var profileJson = ReadJson(source.ProfilePath);
snapshot = ParseProfileSnapshot(profileJson.RootElement, source, cycleRule);
}
@@ -412,22 +412,50 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
return null;
}
private static ClassIslandScheduleCycleRule ParseCycleRule(string? settingsPath, List<string> warnings)
private static ClassIslandScheduleCycleRule ParseCycleRule(
string? settingsPath,
List<string> warnings,
DateOnly? semesterStartDate = null,
int semesterWeekCycle = 1)
{
if (string.IsNullOrWhiteSpace(settingsPath) || !File.Exists(settingsPath))
DateOnly? singleWeekStartDate = semesterStartDate;
int maxCycle = semesterWeekCycle > 1 ? semesterWeekCycle : 4;
var offsetList = new List<int> { -1, -1, 0, 0, 0, 0, 0, 0 };
if (!string.IsNullOrWhiteSpace(settingsPath) && File.Exists(settingsPath))
{
warnings.Add("ClassIsland Settings.json not found, using default cycle rule.");
return new ClassIslandScheduleCycleRule(null, 4, new List<int> { -1, -1, 0, 0, 0 });
using var json = ReadJson(settingsPath);
var root = json.RootElement;
if (!singleWeekStartDate.HasValue)
{
singleWeekStartDate = TryReadDateOnly(root, "SingleWeekStartTime");
}
if (semesterWeekCycle <= 1)
{
maxCycle = TryReadInt(root, "MultiWeekRotationMaxCycle", 4);
}
var settingsOffsetList = ReadIntList(root, "MultiWeekRotationOffset");
if (settingsOffsetList.Count >= 2)
{
offsetList = settingsOffsetList;
}
}
else
{
warnings.Add("ClassIsland Settings.json not found, using semester settings from component.");
}
using var json = ReadJson(settingsPath);
var root = json.RootElement;
var singleWeekStartDate = TryReadDateOnly(root, "SingleWeekStartTime");
var maxCycle = TryReadInt(root, "MultiWeekRotationMaxCycle", 4);
var offsetList = ReadIntList(root, "MultiWeekRotationOffset");
if (offsetList.Count < 2)
if (maxCycle < 2)
{
offsetList = new List<int> { -1, -1, 0, 0, 0 };
maxCycle = 2;
}
while (offsetList.Count <= maxCycle)
{
offsetList.Add(0);
}
return new ClassIslandScheduleCycleRule(
@@ -469,7 +497,10 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
ClassPlanGroups: groups);
}
private static ClassIslandScheduleSnapshot ParseCsesSnapshot(ResolvedSource source)
private static ClassIslandScheduleSnapshot ParseCsesSnapshot(
ResolvedSource source,
DateOnly? semesterStartDate = null,
int semesterWeekCycle = 1)
{
var yaml = File.ReadAllText(source.ProfilePath);
var csesProfile = CsesDeserializer.Deserialize<CsesProfileDto>(yaml) ?? new CsesProfileDto();
@@ -600,12 +631,19 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
[GlobalClassPlanGroupId] = new ClassIslandClassPlanGroup(GlobalClassPlanGroupId, "Global", IsGlobal: true)
};
var maxCycle = semesterWeekCycle > 1 ? semesterWeekCycle : 4;
var offsetList = new List<int> { -1, -1, 0, 0, 0, 0, 0, 0 };
while (offsetList.Count <= maxCycle)
{
offsetList.Add(0);
}
return new ClassIslandScheduleSnapshot(
SourceRootPath: source.SourceRootPath,
ProfilePath: source.ProfilePath,
ProfileFileName: source.ProfileFileName,
LoadedAt: DateTimeOffset.Now,
CycleRule: new ClassIslandScheduleCycleRule(null, 4, new List<int> { -1, -1, 0, 0, 0 }),
CycleRule: new ClassIslandScheduleCycleRule(semesterStartDate, Math.Clamp(maxCycle, 2, 32), offsetList),
SelectedClassPlanGroupId: DefaultClassPlanGroupId,
TempClassPlanGroupId: null,
IsTempClassPlanGroupEnabled: false,

View File

@@ -36,9 +36,17 @@ public sealed class LocalizationService
public string NormalizeLanguageCode(string? languageCode)
{
return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase)
? "en-US"
: "zh-CN";
if (string.IsNullOrWhiteSpace(languageCode))
{
return "zh-CN";
}
return languageCode.ToLowerInvariant() switch
{
"en-us" or "en" => "en-US",
"ja-jp" or "ja" => "ja-JP",
_ => "zh-CN"
};
}
public string GetString(string languageCode, string key, string fallback)

View File

@@ -337,6 +337,14 @@ public sealed class PostHogUsageTelemetryService : IDisposable
["timestamp"] = timestamp.ToString("o"),
["properties"] = new Dictionary<string, object?>
{
["install_id"] = installId,
["app_version"] = TelemetryEnvironmentInfo.GetAppVersion(),
["os_name"] = TelemetryEnvironmentInfo.GetOsName(),
["os_version"] = TelemetryEnvironmentInfo.GetOsVersion(),
["device_model"] = TelemetryEnvironmentInfo.GetDeviceModel(),
["device_arch"] = TelemetryEnvironmentInfo.GetDeviceArchitecture(),
["runtime_version"] = TelemetryEnvironmentInfo.GetRuntimeVersion(),
["language"] = TelemetryEnvironmentInfo.GetSystemLanguage(),
["launch_time_utc"] = timestamp.ToString("o")
}
};

View File

@@ -16,7 +16,13 @@ public enum WallpaperMediaType
}
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
public sealed record WallpaperSettingsState(string? WallpaperPath, string Type, string? Color, string Placement, string? CustomColor = null);
public sealed record WallpaperSettingsState(
string? WallpaperPath,
string Type,
string? Color,
string Placement,
string? CustomColor = null,
int SystemWallpaperRefreshIntervalSeconds = 300);
public sealed record ThemeAppearanceSettingsState(
bool IsNightMode,
string? ThemeColor,

View File

@@ -101,7 +101,9 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
: snapshot.WallpaperPath,
normalizedType,
snapshot.WallpaperColor,
snapshot.WallpaperPlacement);
snapshot.WallpaperPlacement,
CustomColor: null,
SystemWallpaperRefreshIntervalSeconds: NormalizeRefreshInterval(snapshot.SystemWallpaperRefreshIntervalSeconds));
}
public void Save(WallpaperSettingsState state)
@@ -128,6 +130,7 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
? "Fill"
: state.Placement.Trim();
snapshot.SystemWallpaperRefreshIntervalSeconds = NormalizeRefreshInterval(state.SystemWallpaperRefreshIntervalSeconds);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
@@ -136,9 +139,21 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService
nameof(AppSettingsSnapshot.WallpaperPath),
nameof(AppSettingsSnapshot.WallpaperType),
nameof(AppSettingsSnapshot.WallpaperColor),
nameof(AppSettingsSnapshot.WallpaperPlacement)
nameof(AppSettingsSnapshot.WallpaperPlacement),
nameof(AppSettingsSnapshot.SystemWallpaperRefreshIntervalSeconds)
]);
}
private static int NormalizeRefreshInterval(int seconds)
{
return seconds switch
{
<= 0 => 300,
< 30 => 30,
> 86400 => 86400,
_ => seconds
};
}
}
internal sealed class WallpaperMediaService : IWallpaperMediaService

View File

@@ -0,0 +1,65 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Avalonia.Media.Imaging;
using Microsoft.Win32;
namespace LanMountainDesktop.Services;
public interface ISystemWallpaperProvider
{
bool IsSupported { get; }
string? GetWallpaperPath();
event EventHandler? WallpaperChanged;
}
internal sealed class SystemWallpaperProvider : ISystemWallpaperProvider, IDisposable
{
public bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public event EventHandler? WallpaperChanged;
public string? GetWallpaperPath()
{
if (!IsSupported)
{
return null;
}
try
{
using var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Desktop");
var wallpaperPath = key?.GetValue("Wallpaper") as string;
if (string.IsNullOrWhiteSpace(wallpaperPath))
{
return null;
}
if (!File.Exists(wallpaperPath))
{
return null;
}
return wallpaperPath;
}
catch
{
return null;
}
}
public void Dispose()
{
}
}
public static class HostSystemWallpaperProvider
{
private static ISystemWallpaperProvider? _instance;
public static ISystemWallpaperProvider GetOrCreate()
{
return _instance ??= new SystemWallpaperProvider();
}
}

View File

@@ -78,25 +78,6 @@ public sealed class TelemetryIdentityService
}
}
public string RefreshTelemetryId()
{
lock (_syncRoot)
{
EnsureInitialized();
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
snapshot.TelemetryId = GenerateId();
_settingsFacade.Settings.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys: [nameof(AppSettingsSnapshot.TelemetryId)]);
_telemetryId = snapshot.TelemetryId ?? GenerateId();
AppLogger.Info("TelemetryIdentity", $"Telemetry id refreshed. TelemetryId={_telemetryId}");
return _telemetryId;
}
}
public bool MarkBaselineReported()
{
lock (_syncRoot)

View File

@@ -5,13 +5,13 @@ using Avalonia.Media.Imaging;
namespace LanMountainDesktop.Services;
internal static class WallpaperImageBrushFactory
public static class WallpaperImageBrushFactory
{
internal const string Fill = "Fill";
internal const string Fit = "Fit";
internal const string StretchMode = "Stretch";
internal const string Center = "Center";
internal const string Tile = "Tile";
public const string Fill = "Fill";
public const string Fit = "Fit";
public const string StretchMode = "Stretch";
public const string Center = "Center";
public const string Tile = "Tile";
public static string NormalizePlacement(string? placement)
{

View File

@@ -58,9 +58,6 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _telemetryIdDescription = string.Empty;
[ObservableProperty]
private string _refreshTelemetryIdText = string.Empty;
[ObservableProperty]
private string _viewPrivacyPolicyText = string.Empty;
@@ -75,27 +72,6 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
TelemetryId = TelemetryServices.Identity?.TelemetryId ?? string.Empty;
}
[RelayCommand]
private void RefreshTelemetryId()
{
try
{
var identity = TelemetryServices.Identity;
if (identity is null)
{
AppLogger.Warn("PrivacySettings", "Telemetry identity service is unavailable.");
return;
}
TelemetryId = identity.RefreshTelemetryId();
AppLogger.Info("PrivacySettings", $"Telemetry ID refreshed: {TelemetryId}");
}
catch (Exception ex)
{
AppLogger.Warn("PrivacySettings", "Failed to refresh telemetry ID.", ex);
}
}
partial void OnUploadAnonymousCrashDataChanged(bool value)
{
if (_isInitializing)
@@ -137,8 +113,7 @@ public sealed partial class PrivacySettingsPageViewModel : ViewModelBase
TelemetryIdHeader = L("settings.privacy.telemetry_id_title", "Telemetry ID");
TelemetryIdDescription = L(
"settings.privacy.telemetry_id_description",
"A refreshable anonymous identifier used for detailed telemetry sessions.");
RefreshTelemetryIdText = L("settings.privacy.refresh_telemetry_id", "Refresh");
"An anonymous identifier used for detailed telemetry sessions.");
PrivacyPolicyHintPrefix = L("settings.privacy.policy_hint_prefix", "For more details, please ");
ViewPrivacyPolicyText = L("settings.privacy.view_policy", "view our privacy policy");
}

View File

@@ -326,7 +326,8 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
return
[
new SelectionOption("zh-CN", L("settings.region.language_zh", "中文")),
new SelectionOption("en-US", L("settings.region.language_en", "English"))
new SelectionOption("en-US", L("settings.region.language_en", "English")),
new SelectionOption("ja-JP", L("settings.region.language_ja", "日本語"))
];
}

View File

@@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@@ -15,6 +16,7 @@ namespace LanMountainDesktop.ViewModels;
public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly ISystemWallpaperProvider _systemWallpaperProvider;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _isInitializing;
@@ -22,9 +24,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
public WallpaperSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_systemWallpaperProvider = HostSystemWallpaperProvider.GetOrCreate();
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
WallpaperPlacements = CreateWallpaperPlacements();
WallpaperTypes = CreateWallpaperTypes();
RefreshIntervals = CreateRefreshIntervals();
PresetColors = CreatePresetColors();
RefreshLocalizedText();
@@ -35,8 +39,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
public IReadOnlyList<SelectionOption> WallpaperPlacements { get; }
public IReadOnlyList<SelectionOption> WallpaperTypes { get; }
public IReadOnlyList<SelectionOption> RefreshIntervals { get; }
public IReadOnlyList<string> PresetColors { get; }
public bool IsSystemWallpaperSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
[ObservableProperty]
private string _wallpaperPath = string.Empty;
@@ -49,6 +56,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private SelectionOption _selectedWallpaperPlacement = null!;
[ObservableProperty]
private SelectionOption _selectedRefreshInterval = null!;
[ObservableProperty]
private string _wallpaperHeader = string.Empty;
@@ -73,6 +83,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _filePickerTitle = string.Empty;
[ObservableProperty]
private string _systemWallpaperLabel = string.Empty;
[ObservableProperty]
private string _refreshIntervalLabel = string.Empty;
[ObservableProperty]
private string _refreshButtonTooltip = string.Empty;
[ObservableProperty]
private string _systemWallpaperStatus = string.Empty;
[ObservableProperty]
private bool _isImageOrVideo;
@@ -82,13 +104,15 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private bool _isImage;
[ObservableProperty]
private bool _isSystemWallpaper;
[ObservableProperty]
private Bitmap? _previewImage;
[ObservableProperty]
private IBrush? _previewBrush;
// 自定义颜色持久化
[ObservableProperty]
private Color _customColor = Colors.White;
@@ -110,7 +134,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase))
?? WallpaperPlacements[0];
// 加载自定义颜色
var refreshIntervalSeconds = wallpaper.SystemWallpaperRefreshIntervalSeconds;
SelectedRefreshInterval = RefreshIntervals.FirstOrDefault(option =>
GetIntervalSeconds(option.Value) == refreshIntervalSeconds)
?? RefreshIntervals[2];
if (!string.IsNullOrWhiteSpace(wallpaper.CustomColor) && Color.TryParse(wallpaper.CustomColor, out var customColor))
{
CustomColor = customColor;
@@ -119,6 +147,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
UpdateVisibility();
UpdatePreviewFromCurrentSelection();
UpdateSystemWallpaperStatus();
}
partial void OnSelectedWallpaperTypeChanged(SelectionOption value)
@@ -132,8 +161,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private void UpdateVisibility()
{
IsImage = SelectedWallpaperType?.Value == "Image";
IsImageOrVideo = IsImage;
IsImageOrVideo = IsImage || SelectedWallpaperType?.Value == "SystemWallpaper";
IsSolidColor = SelectedWallpaperType?.Value == "SolidColor";
IsSystemWallpaper = SelectedWallpaperType?.Value == "SystemWallpaper";
}
partial void OnSelectedColorChanged(string? value)
@@ -145,13 +175,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
partial void OnCustomColorChanged(Color value)
{
CustomColorBrush = new SolidColorBrush(value);
// 将自定义颜色应用到壁纸
var colorHex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}";
SelectedColor = colorHex;
if (_isInitializing) return;
SaveWallpaper();
}
partial void OnSelectedRefreshIntervalChanged(SelectionOption value)
{
if (_isInitializing) return;
SaveWallpaper();
}
public async Task ImportWallpaperAsync(string sourcePath)
{
var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath);
@@ -170,6 +205,12 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private void UpdatePreviewFromCurrentSelection()
{
if (IsSystemWallpaper)
{
UpdateSystemWallpaperPreview();
return;
}
if (!IsImage)
{
ClearPreviewImage();
@@ -180,10 +221,24 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
UpdatePreviewImage(WallpaperPath);
}
private void UpdatePreviewImage(string path)
private void UpdateSystemWallpaperPreview()
{
var systemPath = _systemWallpaperProvider.GetWallpaperPath();
if (string.IsNullOrWhiteSpace(systemPath))
{
ClearPreviewImage();
SystemWallpaperStatus = L("settings.wallpaper.system.unavailable", "Unable to read system wallpaper");
return;
}
SystemWallpaperStatus = systemPath;
UpdatePreviewImage(systemPath);
}
private void UpdatePreviewImage(string? path)
{
var previousPreview = PreviewImage;
if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path))
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
previousPreview?.Dispose();
PreviewImage = null;
@@ -193,7 +248,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
try
{
using var stream = System.IO.File.OpenRead(path);
using var stream = File.OpenRead(path);
var bitmap = new Bitmap(stream);
PreviewImage = bitmap;
PreviewBrush = WallpaperImageBrushFactory.Create(bitmap, SelectedWallpaperPlacement?.Value);
@@ -215,9 +270,21 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
previousPreview?.Dispose();
}
private void UpdateSystemWallpaperStatus()
{
if (!IsSystemWallpaper) return;
UpdateSystemWallpaperPreview();
}
[RelayCommand]
private void RefreshSystemWallpaper()
{
UpdateSystemWallpaperPreview();
}
partial void OnSelectedWallpaperPlacementChanged(SelectionOption value)
{
if (IsImage && PreviewImage is not null)
if ((IsImage || IsSystemWallpaper) && PreviewImage is not null)
{
PreviewBrush = WallpaperImageBrushFactory.Create(PreviewImage, value?.Value);
}
@@ -236,16 +303,46 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
{
var selectedType = SelectedWallpaperType?.Value ?? "Image";
var selectedPlacement = SelectedWallpaperPlacement?.Value ?? WallpaperImageBrushFactory.Fill;
var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath)
? null
: WallpaperPath;
var refreshIntervalSeconds = GetIntervalSeconds(SelectedRefreshInterval?.Value);
string? normalizedPath;
if (selectedType == "SolidColor" || selectedType == "SystemWallpaper")
{
normalizedPath = null;
}
else
{
normalizedPath = string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath;
}
var customColorHex = $"#{CustomColor.A:X2}{CustomColor.R:X2}{CustomColor.G:X2}{CustomColor.B:X2}";
_settingsFacade.Wallpaper.Save(new WallpaperSettingsState(
normalizedPath,
selectedType,
SelectedColor,
selectedPlacement,
customColorHex));
customColorHex,
refreshIntervalSeconds));
}
private static int GetIntervalSeconds(string? value)
{
return value switch
{
"30s" => 30,
"1m" => 60,
"5m" => 300,
"10m" => 600,
"15m" => 900,
"30m" => 1800,
"1h" => 3600,
"2h" => 7200,
"4h" => 14400,
"8h" => 28800,
"12h" => 43200,
"24h" => 86400,
_ => 300
};
}
private IReadOnlyList<SelectionOption> CreateWallpaperPlacements()
@@ -262,10 +359,36 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
private IReadOnlyList<SelectionOption> CreateWallpaperTypes()
{
return
[
var types = new List<SelectionOption>
{
new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")),
new SelectionOption("SolidColor", L("settings.wallpaper.type.solid_color", "Solid Color"))
};
if (IsSystemWallpaperSupported)
{
types.Add(new SelectionOption("SystemWallpaper", L("settings.wallpaper.type.system", "System Wallpaper")));
}
return types;
}
private IReadOnlyList<SelectionOption> CreateRefreshIntervals()
{
return
[
new SelectionOption("30s", L("settings.wallpaper.refresh.30s", "30 seconds")),
new SelectionOption("1m", L("settings.wallpaper.refresh.1m", "1 minute")),
new SelectionOption("5m", L("settings.wallpaper.refresh.5m", "5 minutes")),
new SelectionOption("10m", L("settings.wallpaper.refresh.10m", "10 minutes")),
new SelectionOption("15m", L("settings.wallpaper.refresh.15m", "15 minutes")),
new SelectionOption("30m", L("settings.wallpaper.refresh.30m", "30 minutes")),
new SelectionOption("1h", L("settings.wallpaper.refresh.1h", "1 hour")),
new SelectionOption("2h", L("settings.wallpaper.refresh.2h", "2 hours")),
new SelectionOption("4h", L("settings.wallpaper.refresh.4h", "4 hours")),
new SelectionOption("8h", L("settings.wallpaper.refresh.8h", "8 hours")),
new SelectionOption("12h", L("settings.wallpaper.refresh.12h", "12 hours")),
new SelectionOption("24h", L("settings.wallpaper.refresh.24h", "24 hours"))
];
}
@@ -289,6 +412,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase
WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop.");
ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper");
FilePickerTitle = L("filepicker.title", "Select wallpaper");
SystemWallpaperLabel = L("settings.wallpaper.system.label", "System Wallpaper");
RefreshIntervalLabel = L("settings.wallpaper.refresh_interval", "Refresh Interval");
RefreshButtonTooltip = L("settings.wallpaper.refresh_now", "Refresh Now");
}
private string L(string key, string fallback)

View File

@@ -27,7 +27,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
ISettingsFacadeService settingsFacade,
LocalizationService localizationService,
ILocationService locationService,
WeatherLocationRefreshService weatherLocationRefreshService)
WeatherLocationRefreshService weatherLocationRefreshService,
bool enableStartupPreviewRefresh = true)
{
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
@@ -52,7 +53,10 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
? LocationReadyText
: LocationUnsupportedText;
_ = RefreshPreviewAsync();
if (enableStartupPreviewRefresh)
{
_ = RefreshPreviewAsync();
}
}
public IReadOnlyList<SelectionOption> LocationModes { get; }
@@ -476,6 +480,65 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase
}
}
internal void ApplyDesignTimePreview()
{
_isInitializing = true;
var previewLocation = new WeatherLocation(
"Shenzhen Nanshan",
"101280601",
22.5431,
114.0579,
"Guangdong, China");
var alternateLocation = new WeatherLocation(
"Shanghai Pudong",
"101020600",
31.2304,
121.4737,
"Shanghai, China");
SelectedLocationMode = LocationModes.FirstOrDefault(option =>
string.Equals(option.Value, "CitySearch", StringComparison.OrdinalIgnoreCase))
?? LocationModes[0];
SearchKeyword = "shenzhen";
SelectedSearchResult = previewLocation;
SearchResults.Clear();
SearchResults.Add(previewLocation);
SearchResults.Add(alternateLocation);
Latitude = previewLocation.Latitude;
Longitude = previewLocation.Longitude;
LocationKey = previewLocation.LocationKey;
LocationName = previewLocation.Name;
AutoRefreshLocation = true;
ExcludedAlerts = "Heat\nThunderstorm";
NoTlsRequests = false;
IsLocationSupported = true;
IsRefreshingLocation = false;
IsRefreshingPreview = false;
_isInitializing = false;
UpdateModeVisibility();
UpdateCurrentLocationSummary();
var preview = XiaomiWeatherVisualResolver.Resolve(
"Partly cloudy",
4,
isNight: false,
_languageCode);
SearchStatus = "2 sample locations are shown for design preview.";
LocationActionStatus = "Using mocked Windows location support in design mode.";
PreviewIcon = HyperOS3WeatherAssetLoader.LoadImage(preview.PrimaryIconAsset);
PreviewLocation = previewLocation.Name;
PreviewTemperature = "24 deg C";
PreviewCondition = preview.DisplayText;
PreviewUpdated = "Updated 09:42";
PreviewStatus = "Preview data is mocked for Avalonia design mode.";
}
private void RefreshLocalizedText()
{
PageTitle = L("settings.weather.title", "Weather");

View File

@@ -2,6 +2,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:material="clr-namespace:Material.Styles;assembly=Material.Styles"
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
mc:Ignorable="d"
x:Class="LanMountainDesktop.Views.ComponentEditors.ClassScheduleComponentEditor">
<StackPanel Spacing="16">
@@ -36,6 +38,45 @@
</StackPanel>
</Border>
<Border Classes="component-editor-card"
Padding="20">
<StackPanel Spacing="16">
<TextBlock x:Name="SemesterSettingsHeaderTextBlock"
Classes="component-editor-section-title" />
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="16">
<StackPanel Spacing="8">
<TextBlock x:Name="SemesterStartDateLabel"
Classes="component-editor-secondary-text" />
<CalendarDatePicker x:Name="SemesterStartDatePicker"
HorizontalAlignment="Stretch"
SelectedDateChanged="OnSemesterStartDateChanged" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="8" Width="160">
<TextBlock x:Name="WeekCycleLabel"
Classes="component-editor-secondary-text" />
<ComboBox x:Name="WeekCycleComboBox"
Classes="component-editor-select"
HorizontalAlignment="Stretch"
SelectionChanged="OnWeekCycleSelectionChanged">
<ComboBoxItem Content="1" Tag="1" />
<ComboBoxItem Content="2" Tag="2" />
<ComboBoxItem Content="3" Tag="3" />
<ComboBoxItem Content="4" Tag="4" />
<ComboBoxItem Content="5" Tag="5" />
<ComboBoxItem Content="6" Tag="6" />
<ComboBoxItem Content="7" Tag="7" />
</ComboBox>
</StackPanel>
</Grid>
<TextBlock x:Name="WeekCycleDescription"
Classes="component-editor-secondary-text"
TextWrapping="Wrap"
Opacity="0.7" />
</StackPanel>
</Border>
<Border Classes="component-editor-card"
Padding="20">
<StackPanel Spacing="12">

View File

@@ -76,6 +76,11 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
FollowSystemColorSchemeItem.Content = L("component.color_scheme.follow_system", "Follow system color scheme");
UseNativeColorSchemeItem.Content = L("component.color_scheme.native", "Use component custom color scheme");
SemesterSettingsHeaderTextBlock.Text = L("schedule.settings.semester.title", "Semester Settings");
SemesterStartDateLabel.Text = L("schedule.settings.semester.start_date", "Semester Start Date");
WeekCycleLabel.Text = L("schedule.settings.semester.week_cycle", "Week Cycle");
WeekCycleDescription.Text = L("schedule.settings.semester.week_cycle_desc", "Set the week rotation cycle for multi-week schedules (e.g., 2 for odd/even weeks).");
AddScheduleButton.Content = L("schedule.settings.add", "Add Schedule");
EmptyStateTextBlock.Text = L("schedule.settings.empty", "No imported schedules yet.");
@@ -85,9 +90,25 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
string.Equals(colorSchemeSource, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase)
? FollowSystemColorSchemeItem
: UseNativeColorSchemeItem;
if (snapshot.SemesterStartDate.HasValue)
{
SemesterStartDatePicker.SelectedDate = snapshot.SemesterStartDate.Value.ToDateTime(TimeOnly.MinValue);
}
var weekCycle = Math.Clamp(snapshot.SemesterWeekCycle, 1, 7);
WeekCycleComboBox.SelectedIndex = weekCycle - 1;
UpdateWeekCycleDescription(weekCycle);
_suppressEvents = false;
}
private void UpdateWeekCycleDescription(int weekCycle)
{
var format = L("schedule.settings.semester.week_cycle_format", "{0}-week rotation");
WeekCycleDescription.Text = string.Format(format, weekCycle);
}
private void OnColorSchemeSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
_ = sender;
@@ -106,6 +127,39 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource));
}
private void OnSemesterStartDateChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressEvents)
{
return;
}
var snapshot = LoadSnapshot();
if (SemesterStartDatePicker.SelectedDate.HasValue)
{
snapshot.SemesterStartDate = DateOnly.FromDateTime(SemesterStartDatePicker.SelectedDate.Value);
}
else
{
snapshot.SemesterStartDate = null;
}
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.SemesterStartDate));
}
private void OnWeekCycleSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressEvents)
{
return;
}
var weekCycle = WeekCycleComboBox.SelectedIndex + 1;
var snapshot = LoadSnapshot();
snapshot.SemesterWeekCycle = weekCycle;
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.SemesterWeekCycle));
UpdateWeekCycleDescription(weekCycle);
}
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
{
_ = sender;
@@ -122,7 +176,15 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
AllowMultiple = false,
FileTypeFilter =
[
new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES Schedule"))
new FilePickerFileType(L("schedule.settings.picker_file_type.all", "ClassIsland Schedule Files"))
{
Patterns = ["*.json", "*.cses", "*.yaml", "*.yml"]
},
new FilePickerFileType(L("schedule.settings.picker_file_type.json", "ClassIsland Profile (JSON)"))
{
Patterns = ["*.json"]
},
new FilePickerFileType(L("schedule.settings.picker_file_type.cses", "CSES Schedule (YAML)"))
{
Patterns = ["*.cses", "*.yaml", "*.yml"]
}

View File

@@ -3,7 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:webview="clr-namespace:AvaloniaWebView;assembly=Avalonia.WebView"
mc:Ignorable="d"
d:DesignWidth="480"
d:DesignHeight="480"
@@ -24,20 +23,27 @@
BorderBrush="#22000000"
BorderThickness="1">
<Grid>
<webview:WebView x:Name="BrowserWebView" />
<Grid x:Name="WebViewPresenter" />
<Border x:Name="UnavailableOverlay"
IsVisible="False"
Background="#CC0F172A"
Padding="16">
<TextBlock x:Name="UnavailableMessageTextBlock"
Foreground="#F8FAFC"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="360"
FontSize="13"
Text="Browser runtime unavailable." />
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="10"
MaxWidth="360">
<fi:SymbolIcon Symbol="Desktop"
FontSize="28"
HorizontalAlignment="Center"
Foreground="#F8FAFC" />
<TextBlock x:Name="UnavailableMessageTextBlock"
Foreground="#F8FAFC"
TextWrapping="Wrap"
TextAlignment="Center"
HorizontalAlignment="Center"
FontSize="13"
Text="Browser runtime unavailable." />
</StackPanel>
</Border>
</Grid>
</Border>

View File

@@ -17,6 +17,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
{
private static readonly Uri DefaultHomeUri = new("https://www.bing.com");
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private double _currentCellSize = 48;
private string _componentId = BuiltInComponentIds.DesktopBrowser;
private string _placementId = string.Empty;
@@ -27,6 +28,7 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool _isEditMode;
private bool _isWebViewActive = true;
private bool _isWebViewFaulted;
private WebView? _browserWebView;
private readonly WebView2RuntimeAvailability _runtimeAvailability;
private bool _isDisposed;
@@ -41,10 +43,15 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
ApplyCellSize(_currentCellSize);
ApplyTheme(force: true);
_runtimeAvailability = WebView2RuntimeProbe.GetAvailability();
_runtimeAvailability = _isDesignModePreview
? new WebView2RuntimeAvailability(
IsAvailable: false,
Version: null,
Message: "WebView preview is disabled in Avalonia design mode.")
: WebView2RuntimeProbe.GetAvailability();
if (_runtimeAvailability.IsAvailable)
{
BrowserWebView.NavigationStarting += OnBrowserWebViewNavigationStarting;
EnsureWebViewCreated();
}
else
{
@@ -69,9 +76,9 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
if (_runtimeAvailability.IsAvailable)
if (_browserWebView is not null)
{
BrowserWebView.NavigationStarting -= OnBrowserWebViewNavigationStarting;
_browserWebView.NavigationStarting -= OnBrowserWebViewNavigationStarting;
}
}
@@ -300,6 +307,13 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void UpdateWebViewActiveState()
{
if (_isDesignModePreview)
{
_isWebViewActive = false;
ApplyRuntimeUnavailableState();
return;
}
if (!_runtimeAvailability.IsAvailable || _isWebViewFaulted)
{
_isWebViewActive = false;
@@ -325,14 +339,21 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void ActivateWebView()
{
EnsureWebViewCreated();
if (_isWebViewFaulted || !_runtimeAvailability.IsAvailable)
{
ApplyRuntimeUnavailableState();
return;
}
BrowserWebView.IsVisible = true;
BrowserWebView.IsHitTestVisible = true;
if (_browserWebView is null)
{
ApplyRuntimeUnavailableState();
return;
}
_browserWebView.IsVisible = true;
_browserWebView.IsHitTestVisible = true;
RefreshButton.IsEnabled = true;
GoButton.IsEnabled = true;
AddressTextBox.IsEnabled = true;
@@ -341,8 +362,11 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void DeactivateWebView(bool clearUrl)
{
BrowserWebView.IsHitTestVisible = false;
BrowserWebView.IsVisible = false;
if (_browserWebView is not null)
{
_browserWebView.IsHitTestVisible = false;
_browserWebView.IsVisible = false;
}
if (clearUrl)
{
@@ -352,9 +376,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool TryReloadWebView(string action)
{
if (_browserWebView is null)
{
return false;
}
try
{
BrowserWebView.Reload();
_browserWebView.Reload();
return true;
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
@@ -366,9 +395,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool TryNavigate(Uri uri, string action)
{
if (_browserWebView is null)
{
return false;
}
try
{
BrowserWebView.Url = uri;
_browserWebView.Url = uri;
return true;
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
@@ -380,9 +414,14 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private void TryClearWebViewUrl()
{
if (_browserWebView is null)
{
return;
}
try
{
BrowserWebView.Url = null;
_browserWebView.Url = null;
}
catch
{
@@ -392,14 +431,20 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
private bool CanUseWebView()
{
return _runtimeAvailability.IsAvailable && !_isWebViewFaulted && _isWebViewActive;
return _runtimeAvailability.IsAvailable &&
!_isWebViewFaulted &&
_isWebViewActive &&
_browserWebView is not null;
}
private void ApplyRuntimeUnavailableState()
{
_isWebViewActive = false;
BrowserWebView.IsVisible = false;
BrowserWebView.IsHitTestVisible = false;
if (_browserWebView is not null)
{
_browserWebView.IsVisible = false;
_browserWebView.IsHitTestVisible = false;
}
RefreshButton.IsEnabled = false;
GoButton.IsEnabled = false;
@@ -414,6 +459,22 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
UnavailableOverlay.IsVisible = true;
}
private void EnsureWebViewCreated()
{
if (_browserWebView is not null || _isDesignModePreview || !_runtimeAvailability.IsAvailable)
{
return;
}
_browserWebView = new WebView
{
IsVisible = false,
IsHitTestVisible = false
};
_browserWebView.NavigationStarting += OnBrowserWebViewNavigationStarting;
WebViewPresenter.Children.Insert(0, _browserWebView);
}
private void EnterFaultedState(string action, Exception ex)
{
_isWebViewFaulted = true;

View File

@@ -253,7 +253,11 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
var today = DateOnly.FromDateTime(now);
var importedSchedulePath = ResolveImportedSchedulePath(componentSettings);
var readResult = _scheduleService.Load(importedSchedulePath);
var readResult = _scheduleService.Load(
importedSchedulePath,
profileFileName: null,
semesterStartDate: componentSettings.SemesterStartDate,
semesterWeekCycle: componentSettings.SemesterWeekCycle);
if (!readResult.Success || readResult.Snapshot is null)
{
_courseItems = Array.Empty<CourseItemViewModel>();

View File

@@ -44,6 +44,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromMinutes(30)
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
@@ -102,12 +103,19 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
News2TitleTextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
RefreshButton.Click += OnRefreshButtonClick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyCellSize(_currentCellSize);
UpdateLanguageCode();
@@ -226,6 +234,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (_isRefreshing)
{
return;
@@ -242,6 +256,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnNewsItem1PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -253,6 +273,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnNewsItem2PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -264,6 +290,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void OnExtraNewsItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed ||
sender is not Control control ||
control.Tag is not int index)
@@ -408,6 +440,55 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
UpdateAdaptiveLayout();
}
private void ApplyDesignTimePreview()
{
_isNightVisual = ResolveNightMode();
_activeNewsItems =
[
new DailyNewsItemSnapshot(
"LanMountain preview mode now shows mocked widget content in Rider.",
null,
"https://example.com/news/preview-1",
null,
"09:30"),
new DailyNewsItemSnapshot(
"Weather, artwork, and plugin market cards render without live network calls.",
null,
"https://example.com/news/preview-2",
null,
"09:10"),
new DailyNewsItemSnapshot(
"Design-time mocks make isolated widget layout tuning much faster.",
null,
"https://example.com/news/preview-3",
null,
"08:55")
];
_newsUrls.Clear();
foreach (var item in _activeNewsItems)
{
_newsUrls.Add(item.Url);
}
UpdateHotHeadlineText(_activeNewsItems[0].Title);
News2TitleTextBlock.Text = NormalizeCompactText(_activeNewsItems[1].Title);
StatusTextBlock.Text = string.Empty;
StatusTextBlock.IsVisible = false;
SetNewsBitmap(0, null);
SetNewsBitmap(1, null);
RenderExtraNewsRows(_activeNewsItems.Skip(2).ToArray());
UpdateNewsInteractionState();
RefreshButton.IsEnabled = false;
RefreshButton.Opacity = 1.0;
RefreshGlyphIcon.Opacity = 0.82;
RefreshLabelTextBlock.Opacity = 0.82;
UpdateAdaptiveLayout();
}
private int ResolveDesiredNewsItemCount()
{
return 2;

View File

@@ -60,6 +60,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
Interval = TimeSpan.FromHours(6)
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
private readonly LocalizationService _localizationService = new();
@@ -85,10 +86,17 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
ArtistTextBlock.FontFamily = MiSansFontFamily;
YearTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ApplyCellSize(_currentCellSize);
UpdateLanguageCode();
@@ -177,6 +185,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
private void OnArtworkPanelPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -188,6 +201,11 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
private void OnInfoPanelPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_isDesignModePreview)
{
return;
}
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
@@ -420,6 +438,36 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
UpdateAdaptiveLayout();
}
private void ApplyDesignTimePreview()
{
DisposeArtworkBitmap();
_currentArtworkSourceUrl = null;
_currentArtworkImageUrl = null;
RootBorder.Background = new SolidColorBrush(Color.Parse("#C6B08B"));
ArtworkPanel.Background = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops = new GradientStops
{
new GradientStop(Color.Parse("#AA8B69"), 0),
new GradientStop(Color.Parse("#5F4B3B"), 1)
}
};
InfoPanel.Background = new SolidColorBrush(Color.Parse("#15181D"));
DateTextBlock.Text = "03/22";
WeekdayTextBlock.Text = "Sunday";
PaintingTitleTextBlock.Text = BuildQuotedTitle("The Starry Night");
ArtistTextBlock.Text = NormalizeCompactText("Vincent van Gogh");
YearTextBlock.Text = "1889 | MoMA";
StatusTextBlock.IsVisible = false;
StatusTextBlock.Text = string.Empty;
UpdateAdaptiveLayout();
}
private void UpdateAdaptiveLayout()
{
var scale = ResolveScale();

View File

@@ -31,6 +31,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
Interval = TimeSpan.FromHours(6)
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
@@ -55,12 +56,19 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
ExampleTranslationTextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
if (_isDesignModePreview)
{
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
RefreshButton.Click += OnRefreshButtonClick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
ActualThemeVariantChanged += OnActualThemeVariantChanged;
ApplyCellSize(_currentCellSize);
UpdateLanguageCode();
@@ -175,6 +183,12 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
{
if (_isDesignModePreview)
{
e.Handled = true;
return;
}
if (_isRefreshing)
{
return;
@@ -284,6 +298,26 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
UpdateAdaptiveLayout();
}
private void ApplyDesignTimePreview()
{
_isNightVisual = ResolveNightMode();
ApplyNightModeVisual();
WordTextBlock.Text = "serendipity";
PronunciationTextBlock.Text = "UK /,seren'dipiti/ | US /,seren'dipiti/";
MeaningTextBlock.Text = "n. finding something valuable by accident; a pleasant surprise.";
ExampleTextBlock.Text = "The widget preview became useful by pure serendipity.";
ExampleTranslationTextBlock.Text = "A mocked sample sentence shown only in design mode.";
StatusTextBlock.Text = string.Empty;
StatusTextBlock.IsVisible = false;
RefreshButton.IsEnabled = false;
RefreshButton.Opacity = 1.0;
RefreshIcon.Opacity = 0.82;
UpdateAdaptiveLayout();
}
private void UpdateAdaptiveLayout()
{
var scale = ResolveScale();

View File

@@ -10,7 +10,7 @@
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
<Border x:Name="RootBorder"
CornerRadius="{DynamicResource DesignCornerRadiusIsland}"
CornerRadius="{DynamicResource DesignCornerRadiusLg}"
Background="#2D5A8E"
ClipToBounds="True"
BorderThickness="0"

View File

@@ -213,10 +213,17 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
ContentGrid.ColumnDefinitions[1].Width = new GridLength(showDial ? dialSize : 0, GridUnitType.Pixel);
}
var leftWidthFactor = Math.Clamp(leftContentWidth / 122d, 0.48, 1.35);
TimeTextBlock.FontSize = Math.Clamp((metrics.PrimaryTemperatureFont * 0.74) * scale * compactFactor * leftWidthFactor, 10, 62);
DateTextBlock.FontSize = Math.Clamp(metrics.SecondaryTextFont * scale * compactFactor * leftWidthFactor, 8, 30);
var weatherIconSize = Math.Clamp(metrics.IconFont * scale * compactFactor * leftWidthFactor, 9, 32);
var timeTextWidth = leftContentWidth * 0.92;
var timeCharCount = 5;
var maxTimeFontSize = timeTextWidth / (timeCharCount * 0.58);
var baseTimeFontSize = Math.Clamp(maxTimeFontSize, 12, 48);
var timeFontSize = Math.Clamp(baseTimeFontSize * scale * compactFactor, 10, 48);
TimeTextBlock.FontSize = timeFontSize;
var dateFontSize = Math.Clamp(timeFontSize * 0.48, 8, 22);
DateTextBlock.FontSize = dateFontSize;
var weatherIconSize = Math.Clamp(dateFontSize * 1.1, 10, 24);
WeatherIconImage.Width = weatherIconSize;
WeatherIconImage.Height = weatherIconSize;
@@ -226,11 +233,11 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
LeftStack.Width = leftContentWidth;
LeftStack.MaxWidth = leftContentWidth;
DateWeatherStack.MaxWidth = leftContentWidth;
TimeTextBlock.MaxWidth = leftContentWidth;
TimeTextBlock.MaxWidth = timeTextWidth;
var showDateLine = leftContentWidth >= Math.Max(40, TimeTextBlock.FontSize * 1.72);
var showDateLine = leftContentWidth >= Math.Max(36, timeFontSize * 1.4) && contentHeight >= 38;
DateWeatherStack.IsVisible = showDateLine;
WeatherIconImage.IsVisible = showDateLine && leftContentWidth >= Math.Max(56, DateTextBlock.FontSize * 2.4);
WeatherIconImage.IsVisible = showDateLine && leftContentWidth >= Math.Max(48, dateFontSize * 3.2);
var dateReservedWidth = WeatherIconImage.IsVisible
? weatherIconSize + DateWeatherStack.Spacing
@@ -477,14 +484,22 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
: CreateBrush("#F8FAFF");
AnalogDialBorder.BorderBrush = CreateBrush(isNightMode ? "#34DDE7FF" : "#12000000");
TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
isNightMode ? "#F8FBFF" : "#10131A",
backgroundSamples,
WeatherTypographyAccessibility.WcagLargeTextContrast);
DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
isNightMode ? "#BCC8DD" : "#7A7E87",
backgroundSamples,
WeatherTypographyAccessibility.WcagNormalTextContrast);
if (isNightMode)
{
TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
"#F8FBFF",
backgroundSamples,
WeatherTypographyAccessibility.WcagLargeTextContrast);
DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush(
"#BCC8DD",
backgroundSamples,
WeatherTypographyAccessibility.WcagNormalTextContrast);
}
else
{
TimeTextBlock.Foreground = CreateBrush("#10131A");
DateTextBlock.Foreground = CreateBrush("#7A7E87");
}
_hourHandLine.Stroke = CreateBrush(isNightMode ? "#F1F5FF" : "#232938");
_minuteHandLine.Stroke = CreateBrush(isNightMode ? "#D6E0F2" : "#2F3749");

View File

@@ -95,6 +95,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
Interval = FluttermotionToken.WeatherAnimationFrameInterval
};
private readonly bool _isDesignModePreview = Design.IsDesignMode;
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new();
@@ -128,11 +129,19 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
InitializeComponent();
InitializeMotionTransform();
SizeChanged += OnSizeChanged;
if (_isDesignModePreview)
{
InitializeParticleVisuals();
ApplyCellSize(_currentCellSize);
ApplyDesignTimePreview();
return;
}
_refreshTimer.Tick += OnRefreshTimerTick;
_backgroundAnimationTimer.Tick += OnBackgroundAnimationTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
InitializeParticleVisuals();
ApplyVisualTheme(WeatherVisualKind.ClearDay);
@@ -512,6 +521,29 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
_latestSnapshot = null;
}
private void ApplyDesignTimePreview()
{
const WeatherVisualKind previewVisualKind = WeatherVisualKind.PartlyCloudyDay;
_languageCode = "en-US";
_latestSnapshot = null;
ApplyVisualTheme(previewVisualKind);
SetWeatherIcon(
HyperOS3WeatherTheme.ResolveHeroIconAsset(HyperOS3WeatherVisualKind.PartlyCloudyDay),
previewVisualKind);
SetLoadingSkeleton(false);
CityTextBlock.Text = "Shenzhen Bay";
ConditionTextBlock.Text = "Partly cloudy";
TemperatureTextBlock.Text = "24°";
RangeTextBlock.Text = "28°/20°";
ResetAnimationState();
ResetParticles();
ApplyAdaptiveTypography();
}
private void ApplyVisualTheme(WeatherVisualKind kind)
{
_activeVisualKind = kind;

View File

@@ -13,6 +13,7 @@ using Avalonia.VisualTree;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.DesktopEditing;
using LanMountainDesktop.Host.Abstractions;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
@@ -34,12 +35,6 @@ public partial class MainWindow
private const string DesktopComponentContentHostTag = "desktop-component-content-host";
private const string DesktopComponentResizeHandleTag = "desktop-component-resize-handle";
private bool _isDesktopComponentDragActive;
private DesktopComponentDragState? _desktopComponentDrag;
private Border? _desktopComponentDragGhost;
private bool _isDesktopComponentResizeActive;
private DesktopComponentResizeState? _desktopComponentResize;
private string? _componentLibraryActiveCategoryId;
private int _componentLibraryCategoryIndex;
private int _componentLibraryComponentIndex;
@@ -58,43 +53,6 @@ public partial class MainWindow
private Point _componentLibraryComponentGestureCurrentPoint;
private double _componentLibraryComponentGestureBaseOffset;
private enum DesktopComponentDragKind
{
None,
NewFromLibrary,
MoveExisting
}
private sealed class DesktopComponentDragState
{
public DesktopComponentDragKind Kind { get; init; }
public string ComponentId { get; init; } = string.Empty;
public string PlacementId { get; init; } = string.Empty;
public int PageIndex { get; init; }
public int WidthCells { get; init; }
public int HeightCells { get; init; }
public Point PointerOffset { get; init; }
public Border? SourceHost { get; init; }
public int TargetRow { get; set; }
public int TargetColumn { get; set; }
}
private sealed class DesktopComponentResizeState
{
public string PlacementId { get; init; } = string.Empty;
public string ComponentId { get; init; } = string.Empty;
public Border SourceHost { get; init; } = null!;
public int StartWidthCells { get; init; }
public int StartHeightCells { get; init; }
public int MinWidthCells { get; init; }
public int MinHeightCells { get; init; }
public int MaxWidthCells { get; init; }
public int MaxHeightCells { get; init; }
public Point StartPointerInViewport { get; init; }
public int CurrentWidthCells { get; set; }
public int CurrentHeightCells { get; set; }
}
private sealed record ComponentLibraryCategory(
string Id,
Symbol Icon,
@@ -495,6 +453,7 @@ public partial class MainWindow
_isComponentLibraryOpen = true;
UpdateDesktopComponentHostEditState();
ShowComponentLibraryCategoryView();
RestoreComponentLibraryAfterDesktopEdit();
ComponentLibraryWindow.IsVisible = true;
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
@@ -509,6 +468,7 @@ public partial class MainWindow
BuildComponentLibraryCategoryPages();
ComponentLibraryWindow.Opacity = 1;
SyncComponentLibraryCollapseExpandedState();
}, DispatcherPriority.Background);
}
@@ -519,6 +479,7 @@ public partial class MainWindow
return;
}
RestoreComponentLibraryAfterDesktopEdit();
_isComponentLibraryOpen = false;
CancelDesktopComponentDrag();
CancelDesktopComponentResize(restoreOriginalSpan: true);
@@ -1971,7 +1932,7 @@ public partial class MainWindow
private void OnDesktopComponentHostPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!_isComponentLibraryOpen || _isDesktopComponentDragActive || _isDesktopComponentResizeActive)
if (!_isComponentLibraryOpen || HasActiveDesktopEditSession)
{
return;
}
@@ -2003,7 +1964,7 @@ public partial class MainWindow
if (IsPointerOnSelectedFrameBorder(host, pointerInHost))
{
BeginDesktopComponentResizeDrag(host, placement, e);
if (_isDesktopComponentResizeActive)
if (IsDesktopEditResizeMode)
{
e.Handled = true;
}
@@ -2065,10 +2026,9 @@ public partial class MainWindow
private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e)
{
if (_isDesktopComponentResizeActive ||
DesktopEditDragLayer is null ||
if (HasActiveDesktopEditSession ||
DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
!TryGetCurrentDesktopGridGeometry(out var grid) ||
!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor))
{
return;
@@ -2082,27 +2042,32 @@ public partial class MainWindow
placement.HeightCells));
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
var pitch = CurrentDesktopPitch;
var topLeft = new Point(placement.Column * pitch, placement.Row * pitch);
var pointerOffset = pointerInViewport - topLeft;
_desktopEditOriginalRect = DesktopPlacementMath.GetCellRect(grid, placement.Column, placement.Row, widthCells, heightCells);
_desktopEditStartRow = placement.Row;
_desktopEditStartColumn = placement.Column;
var pointerOffset = DesktopPlacementMath.Subtract(
pointerInViewport,
new Point(_desktopEditOriginalRect.X, _desktopEditOriginalRect.Y));
sourceHost.Opacity = 0.35;
_desktopEditSession = DesktopEditSession.CreateDraggingExisting(
placement.ComponentId,
placement.PlacementId,
placement.PageIndex,
widthCells,
heightCells,
pointerInViewport,
pointerOffset,
GetComponentLibraryBoundsInViewport());
_desktopComponentDrag = new DesktopComponentDragState
{
Kind = DesktopComponentDragKind.MoveExisting,
ComponentId = placement.ComponentId,
PlacementId = placement.PlacementId,
PageIndex = placement.PageIndex,
WidthCells = widthCells,
HeightCells = heightCells,
PointerOffset = pointerOffset,
SourceHost = sourceHost
};
_isDesktopComponentDragActive = true;
EnsureDesktopComponentDragGhost(placement.ComponentId, widthCells, heightCells);
UpdateDesktopComponentDragVisual(pointerInViewport);
CollapseComponentLibraryForDesktopEdit(ResolveDesktopEditTitle(placement.ComponentId));
SetDesktopEditSourceHost(sourceHost, 0.22);
EnsureDesktopEditOverlayPresenter();
UpdateDesktopEditOverlayMetadata(placement.ComponentId, widthCells, heightCells, L("component.move", "Move"));
_desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect);
_desktopEditOverlayPresenter?.SetCandidateRect(_desktopEditOriginalRect);
_desktopEditOverlayPresenter?.SetInvalid(false);
_desktopEditOverlayPresenter?.Show(DesktopEditGhostVisualStyle.StandardLift);
UpdateDesktopEditSession(pointerInViewport);
e.Pointer.Capture(this);
}
@@ -2110,9 +2075,7 @@ public partial class MainWindow
private void BeginDesktopComponentNewDrag(string componentId, PointerPressedEventArgs e)
{
if (!_isComponentLibraryOpen ||
_isDesktopComponentDragActive ||
_isDesktopComponentResizeActive ||
DesktopEditDragLayer is null ||
HasActiveDesktopEditSession ||
DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor) ||
@@ -2128,74 +2091,35 @@ public partial class MainWindow
runtimeDescriptor.Definition.MinWidthCells,
runtimeDescriptor.Definition.MinHeightCells));
// Center the component under the pointer while dragging from the library.
var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize + Math.Max(0, widthCells - 1) * _currentDesktopCellGap);
var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize + Math.Max(0, heightCells - 1) * _currentDesktopCellGap);
var pointerOffset = new Point(
ghostWidth * 0.5,
ghostHeight * 0.5);
_desktopComponentDrag = new DesktopComponentDragState
{
Kind = DesktopComponentDragKind.NewFromLibrary,
ComponentId = componentId,
PageIndex = _currentDesktopSurfaceIndex,
WidthCells = widthCells,
HeightCells = heightCells,
PointerOffset = pointerOffset
};
_isDesktopComponentDragActive = true;
EnsureDesktopComponentDragGhost(componentId, widthCells, heightCells);
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
UpdateDesktopComponentDragVisual(pointerInViewport);
var previewSize = GetComponentPixelSize(widthCells, heightCells, _currentDesktopCellSize, _currentDesktopCellGap);
var pointerOffset = new Point(previewSize.Width * 0.5, previewSize.Height * 0.5);
_desktopEditOriginalRect = new Rect(
DesktopPlacementMath.Subtract(pointerInViewport, pointerOffset),
previewSize);
_desktopEditSession = DesktopEditSession.CreatePendingNew(
componentId,
_currentDesktopSurfaceIndex,
widthCells,
heightCells,
pointerInViewport,
pointerOffset,
GetComponentLibraryBoundsInViewport());
EnsureDesktopEditOverlayPresenter();
UpdateDesktopEditOverlayMetadata(componentId, widthCells, heightCells, L("component_library.drag_hint", "Drag to place"));
_desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect);
_desktopEditOverlayPresenter?.SetCandidateRect(null);
_desktopEditOverlayPresenter?.SetInvalid(false);
e.Pointer.Capture(this);
}
private void EnsureDesktopComponentDragGhost(string componentId, int widthCells, int heightCells)
{
if (DesktopEditDragLayer is null)
{
return;
}
DesktopEditDragLayer.Children.Clear();
var ghostWidth = Math.Max(1, widthCells * _currentDesktopCellSize + Math.Max(0, widthCells - 1) * _currentDesktopCellGap);
var ghostHeight = Math.Max(1, heightCells * _currentDesktopCellSize + Math.Max(0, heightCells - 1) * _currentDesktopCellGap);
var ghostContent = CreateDesktopComponentControl(componentId);
if (ghostContent is not null)
{
ghostContent.IsHitTestVisible = false;
}
var visualInset = GetDesktopComponentVisualInset(widthCells, heightCells);
_desktopComponentDragGhost = new Border
{
Width = ghostWidth,
Height = ghostHeight,
CornerRadius = new CornerRadius(Math.Clamp(_currentDesktopCellSize * 0.45, 16, 36)),
Background = new SolidColorBrush(Color.Parse("#331E40AF")),
BorderBrush = GetThemeBrush("AdaptiveAccentBrush"),
BorderThickness = new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3)),
Padding = visualInset,
ClipToBounds = true,
Child = ghostContent,
Opacity = 0.92,
IsHitTestVisible = false
};
DesktopEditDragLayer.Children.Add(_desktopComponentDragGhost);
}
private void OnDesktopComponentResizeHandlePointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!_isComponentLibraryOpen ||
_isDesktopComponentDragActive ||
_isDesktopComponentResizeActive ||
HasActiveDesktopEditSession ||
DesktopPagesViewport is null ||
sender is not Border handle ||
!e.GetCurrentPoint(handle).Properties.IsLeftButtonPressed)
@@ -2218,7 +2142,7 @@ public partial class MainWindow
SetSelectedDesktopComponent(host);
BeginDesktopComponentResizeDrag(host, placement, e);
if (_isDesktopComponentResizeActive)
if (IsDesktopEditResizeMode)
{
e.Handled = true;
}
@@ -2229,10 +2153,12 @@ public partial class MainWindow
DesktopComponentPlacementSnapshot placement,
PointerPressedEventArgs e)
{
if (DesktopPagesViewport is null ||
if (HasActiveDesktopEditSession ||
DesktopPagesViewport is null ||
_currentDesktopCellSize <= 0 ||
!_componentRuntimeRegistry.TryGetDescriptor(placement.ComponentId, out var runtimeDescriptor) ||
!_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
!_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid) ||
!TryGetCurrentDesktopGridGeometry(out var grid))
{
return;
}
@@ -2259,152 +2185,59 @@ public partial class MainWindow
}
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
_desktopComponentResize = new DesktopComponentResizeState
_desktopEditOriginalRect = DesktopPlacementMath.GetCellRect(
grid,
placement.Column,
placement.Row,
startSpan.WidthCells,
startSpan.HeightCells);
_desktopEditStartWidthCells = startSpan.WidthCells;
_desktopEditStartHeightCells = startSpan.HeightCells;
_desktopEditMinWidthCells = Math.Max(1, Math.Min(minSpan.WidthCells, maxWidthCells));
_desktopEditMinHeightCells = Math.Max(1, Math.Min(minSpan.HeightCells, maxHeightCells));
_desktopEditMaxWidthCells = maxWidthCells;
_desktopEditMaxHeightCells = maxHeightCells;
_desktopEditResizeMode = runtimeDescriptor.Definition.ResizeMode;
_desktopEditSession = DesktopEditSession.CreateResizingExisting(
placement.ComponentId,
placement.PlacementId,
placement.PageIndex,
startSpan.WidthCells,
startSpan.HeightCells,
pointerInViewport,
GetComponentLibraryBoundsInViewport()) with
{
PlacementId = placement.PlacementId,
ComponentId = placement.ComponentId,
SourceHost = sourceHost,
StartWidthCells = startSpan.WidthCells,
StartHeightCells = startSpan.HeightCells,
MinWidthCells = Math.Max(1, Math.Min(minSpan.WidthCells, maxWidthCells)),
MinHeightCells = Math.Max(1, Math.Min(minSpan.HeightCells, maxHeightCells)),
MaxWidthCells = maxWidthCells,
MaxHeightCells = maxHeightCells,
StartPointerInViewport = pointerInViewport,
CurrentWidthCells = startSpan.WidthCells,
CurrentHeightCells = startSpan.HeightCells
TargetRow = placement.Row,
TargetColumn = placement.Column
};
_isDesktopComponentResizeActive = true;
sourceHost.Opacity = 0.96;
CollapseComponentLibraryForDesktopEdit(ResolveDesktopEditTitle(placement.ComponentId));
SetDesktopEditSourceHost(sourceHost, 0.22);
EnsureDesktopEditOverlayPresenter();
UpdateDesktopEditOverlayMetadata(placement.ComponentId, startSpan.WidthCells, startSpan.HeightCells, L("component.resize", "Resize"));
_desktopEditOverlayPresenter?.SetPreviewRect(_desktopEditOriginalRect);
_desktopEditOverlayPresenter?.SetCandidateRect(_desktopEditOriginalRect);
_desktopEditOverlayPresenter?.SetInvalid(false);
_desktopEditOverlayPresenter?.Show(DesktopEditGhostVisualStyle.StandardLift);
UpdateDesktopEditSession(pointerInViewport);
e.Pointer.Capture(this);
}
private void UpdateDesktopComponentResizeVisual(Point pointerInViewport)
{
if (_desktopComponentResize is null)
{
return;
}
var pitch = CurrentDesktopPitch;
if (pitch <= 0 ||
_desktopComponentResize.StartWidthCells <= 0 ||
_desktopComponentResize.StartHeightCells <= 0)
{
return;
}
var deltaX = pointerInViewport.X - _desktopComponentResize.StartPointerInViewport.X;
var deltaY = pointerInViewport.Y - _desktopComponentResize.StartPointerInViewport.Y;
int widthCells;
int heightCells;
if (GetComponentResizeMode(_desktopComponentResize.ComponentId) == DesktopComponentResizeMode.Free)
{
widthCells = Math.Clamp(
(int)Math.Round(_desktopComponentResize.StartWidthCells + deltaX / pitch),
_desktopComponentResize.MinWidthCells,
_desktopComponentResize.MaxWidthCells);
heightCells = Math.Clamp(
(int)Math.Round(_desktopComponentResize.StartHeightCells + deltaY / pitch),
_desktopComponentResize.MinHeightCells,
_desktopComponentResize.MaxHeightCells);
}
else
{
var widthScale = (_desktopComponentResize.StartWidthCells + deltaX / pitch) / _desktopComponentResize.StartWidthCells;
var heightScale = (_desktopComponentResize.StartHeightCells + deltaY / pitch) / _desktopComponentResize.StartHeightCells;
var proposedScale = Math.Max(widthScale, heightScale);
var minScale = Math.Max(
(double)_desktopComponentResize.MinWidthCells / _desktopComponentResize.StartWidthCells,
(double)_desktopComponentResize.MinHeightCells / _desktopComponentResize.StartHeightCells);
var maxScale = Math.Min(
(double)_desktopComponentResize.MaxWidthCells / _desktopComponentResize.StartWidthCells,
(double)_desktopComponentResize.MaxHeightCells / _desktopComponentResize.StartHeightCells);
if (double.IsNaN(proposedScale) || double.IsInfinity(proposedScale))
{
proposedScale = minScale;
}
if (maxScale < minScale)
{
maxScale = minScale;
}
var scale = Math.Clamp(proposedScale, minScale, maxScale);
widthCells = Math.Clamp(
(int)Math.Round(_desktopComponentResize.StartWidthCells * scale),
_desktopComponentResize.MinWidthCells,
_desktopComponentResize.MaxWidthCells);
heightCells = Math.Clamp(
(int)Math.Round(_desktopComponentResize.StartHeightCells * scale),
_desktopComponentResize.MinHeightCells,
_desktopComponentResize.MaxHeightCells);
}
var normalized = NormalizeComponentCellSpan(_desktopComponentResize.ComponentId, (widthCells, heightCells));
widthCells = Math.Clamp(normalized.WidthCells, _desktopComponentResize.MinWidthCells, _desktopComponentResize.MaxWidthCells);
heightCells = Math.Clamp(normalized.HeightCells, _desktopComponentResize.MinHeightCells, _desktopComponentResize.MaxHeightCells);
_desktopComponentResize.CurrentWidthCells = widthCells;
_desktopComponentResize.CurrentHeightCells = heightCells;
Grid.SetColumnSpan(_desktopComponentResize.SourceHost, widthCells);
Grid.SetRowSpan(_desktopComponentResize.SourceHost, heightCells);
}
private bool TryCompleteDesktopComponentResize(Point pointerInViewport)
{
if (_desktopComponentResize is null)
{
return false;
}
UpdateDesktopComponentResizeVisual(pointerInViewport);
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
string.Equals(p.PlacementId, _desktopComponentResize.PlacementId, StringComparison.OrdinalIgnoreCase));
if (placement is null)
{
return false;
}
var before = ClonePlacementSnapshot(placement);
var widthCells = Math.Max(1, _desktopComponentResize.CurrentWidthCells);
var heightCells = Math.Max(1, _desktopComponentResize.CurrentHeightCells);
var changed = placement.WidthCells != widthCells || placement.HeightCells != heightCells;
placement.WidthCells = widthCells;
placement.HeightCells = heightCells;
ApplyDesktopEditStateToHost(_desktopComponentResize.SourceHost, _isComponentLibraryOpen);
if (changed)
{
PersistSettings();
TelemetryServices.Usage?.TrackDesktopComponentResized(before, ClonePlacementSnapshot(placement), "component.resize");
}
return true;
}
private void CancelDesktopComponentResize(bool restoreOriginalSpan)
{
if (!_isDesktopComponentResizeActive || _desktopComponentResize is null)
if (!IsDesktopEditResizeMode && !_isDesktopEditCommitPending)
{
return;
}
if (restoreOriginalSpan)
if (restoreOriginalSpan && _desktopEditSourceHost is not null)
{
Grid.SetColumnSpan(_desktopComponentResize.SourceHost, _desktopComponentResize.StartWidthCells);
Grid.SetRowSpan(_desktopComponentResize.SourceHost, _desktopComponentResize.StartHeightCells);
Grid.SetColumnSpan(_desktopEditSourceHost, Math.Max(1, _desktopEditStartWidthCells));
Grid.SetRowSpan(_desktopEditSourceHost, Math.Max(1, _desktopEditStartHeightCells));
}
_desktopComponentResize.SourceHost.Opacity = 1;
ApplyDesktopEditStateToHost(_desktopComponentResize.SourceHost, _isComponentLibraryOpen);
_desktopComponentResize = null;
_isDesktopComponentResizeActive = false;
CancelDesktopEditSession(animate: false);
}
private void OnDesktopComponentDragPointerMoved(object? sender, PointerEventArgs e)
@@ -2414,18 +2247,12 @@ public partial class MainWindow
return;
}
if (_isDesktopComponentResizeActive && _desktopComponentResize is not null)
{
UpdateDesktopComponentResizeVisual(e.GetPosition(DesktopPagesViewport));
return;
}
if (!_isDesktopComponentDragActive || _desktopComponentDrag is null)
if (!HasActiveDesktopEditSession || _isDesktopEditCommitPending)
{
return;
}
UpdateDesktopComponentDragVisual(e.GetPosition(DesktopPagesViewport));
UpdateDesktopEditSession(e.GetPosition(DesktopPagesViewport));
}
private void OnDesktopComponentDragPointerReleased(object? sender, PointerReleasedEventArgs e)
@@ -2435,28 +2262,18 @@ public partial class MainWindow
return;
}
if (_isDesktopComponentResizeActive && _desktopComponentResize is not null)
{
var resizePointerInViewport = e.GetPosition(DesktopPagesViewport);
var resizeSuccess = TryCompleteDesktopComponentResize(resizePointerInViewport);
CancelDesktopComponentResize(restoreOriginalSpan: !resizeSuccess);
e.Pointer.Capture(null);
if (resizeSuccess)
{
e.Handled = true;
}
return;
}
if (!_isDesktopComponentDragActive || _desktopComponentDrag is null)
if (!HasActiveDesktopEditSession)
{
return;
}
var pointerInViewport = e.GetPosition(DesktopPagesViewport);
var success = TryCompleteDesktopComponentDrag(pointerInViewport);
CancelDesktopComponentDrag();
var success = CompleteDesktopEditSession(pointerInViewport);
if (!success)
{
CancelDesktopEditSession(animate: !_desktopEditSession.IsPendingNew);
}
e.Pointer.Capture(null);
if (success)
{
@@ -2466,142 +2283,56 @@ public partial class MainWindow
private void OnDesktopComponentDragPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
if (_isDesktopComponentResizeActive)
{
CancelDesktopComponentResize(restoreOriginalSpan: true);
return;
}
if (!_isDesktopComponentDragActive)
if (_isDesktopEditCommitPending)
{
return;
}
CancelDesktopComponentDrag();
}
private void UpdateDesktopComponentDragVisual(Point pointerInViewport)
{
if (_desktopComponentDragGhost is null || _desktopComponentDrag is null || DesktopPagesViewport is null)
if (!HasActiveDesktopEditSession)
{
return;
}
var withinViewport =
pointerInViewport.X >= 0 &&
pointerInViewport.Y >= 0 &&
pointerInViewport.X <= DesktopPagesViewport.Bounds.Width &&
pointerInViewport.Y <= DesktopPagesViewport.Bounds.Height;
if (!withinViewport ||
!TryGetDesktopComponentDropCell(pointerInViewport, _desktopComponentDrag, out var row, out var column))
{
_desktopComponentDragGhost.IsVisible = false;
return;
}
_desktopComponentDragGhost.IsVisible = true;
_desktopComponentDrag.TargetRow = row;
_desktopComponentDrag.TargetColumn = column;
var pitch = CurrentDesktopPitch;
Canvas.SetLeft(_desktopComponentDragGhost, column * pitch);
Canvas.SetTop(_desktopComponentDragGhost, row * pitch);
}
private bool TryGetDesktopComponentDropCell(
Point pointerInViewport,
DesktopComponentDragState state,
out int row,
out int column)
{
row = 0;
column = 0;
if (_currentDesktopCellSize <= 0 ||
_currentDesktopSurfaceIndex < 0 ||
_currentDesktopSurfaceIndex >= _desktopPageCount ||
!_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid))
{
return false;
}
var maxColumns = pageGrid.ColumnDefinitions.Count;
var maxRows = pageGrid.RowDefinitions.Count;
if (maxColumns <= 0 || maxRows <= 0)
{
return false;
}
var pitch = CurrentDesktopPitch;
if (pitch <= 0)
{
return false;
}
var x = pointerInViewport.X - state.PointerOffset.X;
var y = pointerInViewport.Y - state.PointerOffset.Y;
column = (int)Math.Floor(x / pitch);
row = (int)Math.Floor(y / pitch);
column = Math.Clamp(column, 0, Math.Max(0, maxColumns - state.WidthCells));
row = Math.Clamp(row, 0, Math.Max(0, maxRows - state.HeightCells));
return true;
}
private bool TryCompleteDesktopComponentDrag(Point pointerInViewport)
{
if (_desktopComponentDrag is null ||
_currentDesktopCellSize <= 0 ||
_currentDesktopSurfaceIndex < 0 ||
_currentDesktopSurfaceIndex >= _desktopPageCount)
{
return false;
}
if (!TryGetDesktopComponentDropCell(pointerInViewport, _desktopComponentDrag, out var row, out var column))
{
return false;
}
switch (_desktopComponentDrag.Kind)
{
case DesktopComponentDragKind.NewFromLibrary:
PlaceDesktopComponentOnPage(_desktopComponentDrag.ComponentId, _currentDesktopSurfaceIndex, row, column);
return true;
case DesktopComponentDragKind.MoveExisting:
return TryMoveExistingDesktopComponent(_desktopComponentDrag.PlacementId, row, column);
default:
return false;
}
CancelDesktopEditSession(animate: !_desktopEditSession.IsPendingNew);
}
private bool TryMoveExistingDesktopComponent(string placementId, int row, int column)
{
if (string.IsNullOrWhiteSpace(placementId) ||
_desktopComponentDrag?.SourceHost is null ||
_desktopComponentDrag.Kind != DesktopComponentDragKind.MoveExisting)
if (string.IsNullOrWhiteSpace(placementId))
{
return false;
}
var placement = _desktopComponentPlacements.FirstOrDefault(p =>
string.Equals(p.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
if (placement is null)
if (!TryGetDesktopPlacementById(placementId, out var placement))
{
return false;
}
var before = ClonePlacementSnapshot(placement);
if (!DesktopPlacementMath.HasCellPositionChanged(placement.Row, placement.Column, row, column))
{
return false;
}
var host = _desktopEditSourceHost;
if (host is null &&
_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
{
host = pageGrid.Children
.OfType<Border>()
.FirstOrDefault(candidate => string.Equals(candidate.Tag as string, placementId, StringComparison.OrdinalIgnoreCase));
}
placement.Row = Math.Max(0, row);
placement.Column = Math.Max(0, column);
Grid.SetRow(_desktopComponentDrag.SourceHost, placement.Row);
Grid.SetColumn(_desktopComponentDrag.SourceHost, placement.Column);
if (host is not null)
{
Grid.SetRow(host, placement.Row);
Grid.SetColumn(host, placement.Column);
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
}
_desktopComponentDrag.SourceHost.Opacity = 1;
ApplyDesktopEditStateToHost(_desktopComponentDrag.SourceHost, _isComponentLibraryOpen);
PersistSettings();
TelemetryServices.Usage?.TrackDesktopComponentMoved(before, ClonePlacementSnapshot(placement), "component.move");
return true;
@@ -2609,26 +2340,12 @@ public partial class MainWindow
private void CancelDesktopComponentDrag()
{
if (!_isDesktopComponentDragActive)
if (!IsDesktopEditDragMode && !_isDesktopEditCommitPending)
{
return;
}
if (_desktopComponentDrag?.SourceHost is not null)
{
_desktopComponentDrag.SourceHost.Opacity = 1;
ApplyDesktopEditStateToHost(_desktopComponentDrag.SourceHost, _isComponentLibraryOpen);
}
_desktopComponentDrag = null;
_isDesktopComponentDragActive = false;
if (DesktopEditDragLayer is not null)
{
DesktopEditDragLayer.Children.Clear();
}
_desktopComponentDragGhost = null;
CancelDesktopEditSession(animate: false);
}
private void ShowComponentLibraryCategoryView()
@@ -3158,7 +2875,7 @@ public partial class MainWindow
}
BeginDesktopComponentNewDrag(componentId, e);
if (_isDesktopComponentDragActive)
if (HasActiveDesktopEditSession)
{
e.Handled = true;
}
@@ -3171,7 +2888,7 @@ public partial class MainWindow
private void OnComponentLibraryWindowPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ComponentLibraryWindow is null || !_isComponentLibraryOpen)
if (ComponentLibraryWindow is null || !_isComponentLibraryOpen || IsComponentLibraryTemporarilyCollapsedForDesktopEdit())
{
return;
}
@@ -3192,6 +2909,17 @@ public partial class MainWindow
private void OnComponentLibraryWindowPointerMoved(object? sender, PointerEventArgs e)
{
if (IsComponentLibraryTemporarilyCollapsedForDesktopEdit())
{
if (_isComponentLibraryWindowDragging)
{
_isComponentLibraryWindowDragging = false;
e.Pointer.Capture(null);
}
return;
}
if (!_isComponentLibraryWindowDragging || ComponentLibraryWindow is null)
{
return;
@@ -3208,11 +2936,23 @@ public partial class MainWindow
);
ComponentLibraryWindow.Margin = newMargin;
SyncComponentLibraryCollapseExpandedState();
e.Handled = true;
}
private void OnComponentLibraryWindowPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (IsComponentLibraryTemporarilyCollapsedForDesktopEdit())
{
if (_isComponentLibraryWindowDragging)
{
_isComponentLibraryWindowDragging = false;
e.Pointer.Capture(null);
}
return;
}
if (!_isComponentLibraryWindowDragging)
{
return;
@@ -3399,6 +3139,7 @@ public partial class MainWindow
var margin = ComponentLibraryWindow.Margin;
_savedComponentLibraryMargin = margin;
_isComponentLibraryWindowPositionCustomized = true;
SyncComponentLibraryCollapseExpandedState();
}
private void RestoreComponentLibraryWindowPosition()
@@ -3409,6 +3150,7 @@ public partial class MainWindow
}
ComponentLibraryWindow.Margin = _savedComponentLibraryMargin;
SyncComponentLibraryCollapseExpandedState();
}
private Thickness _savedComponentLibraryMargin = new Thickness(24, 24, 24, 100);

View File

@@ -0,0 +1,713 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.DesktopEditing;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
namespace LanMountainDesktop.Views;
public partial class MainWindow
{
private static readonly TimeSpan DesktopEditOverlayAnimationDuration = FluttermotionToken.Fast;
private DesktopEditSession _desktopEditSession;
private DesktopEditOverlayPresenter? _desktopEditOverlayPresenter;
private Border? _desktopEditSourceHost;
private Rect _desktopEditOriginalRect;
private int _desktopEditStartRow;
private int _desktopEditStartColumn;
private int _desktopEditStartWidthCells;
private int _desktopEditStartHeightCells;
private int _desktopEditMinWidthCells;
private int _desktopEditMinHeightCells;
private int _desktopEditMaxWidthCells;
private int _desktopEditMaxHeightCells;
private DesktopComponentResizeMode _desktopEditResizeMode;
private int _desktopEditOverlayVersion;
private int _desktopEditCommitVersion;
private bool _isDesktopEditCommitPending;
private ComponentLibraryCollapsePresenter? _componentLibraryCollapsePresenter;
private bool HasActiveDesktopEditSession => _desktopEditSession.IsActive || _isDesktopEditCommitPending;
private bool IsDesktopEditDragMode =>
_desktopEditSession.Mode is DesktopEditSessionMode.PendingNew or DesktopEditSessionMode.DraggingNew or DesktopEditSessionMode.DraggingExisting;
private bool IsDesktopEditResizeMode =>
_desktopEditSession.Mode == DesktopEditSessionMode.ResizingExisting;
private void EnsureDesktopEditOverlayPresenter()
{
if (DesktopEditDragLayer is null)
{
return;
}
_desktopEditOverlayPresenter ??= new DesktopEditOverlayPresenter();
if (!DesktopEditDragLayer.Children.Contains(_desktopEditOverlayPresenter.Root))
{
DesktopEditDragLayer.Children.Clear();
DesktopEditDragLayer.Children.Add(_desktopEditOverlayPresenter.Root);
}
UpdateDesktopEditOverlayViewportSize();
}
private void UpdateDesktopEditOverlayViewportSize()
{
if (_desktopEditOverlayPresenter is null)
{
return;
}
var width = Math.Max(
DesktopPagesViewport?.Bounds.Width ?? 0,
DesktopEditDragLayer?.Bounds.Width ?? 0);
var height = Math.Max(
DesktopPagesViewport?.Bounds.Height ?? 0,
DesktopEditDragLayer?.Bounds.Height ?? 0);
if (width <= 0 || height <= 0)
{
return;
}
_desktopEditOverlayPresenter.SetViewportSize(new Size(width, height));
}
private void EnsureComponentLibraryCollapsePresenter()
{
if (_componentLibraryCollapsePresenter is not null || ComponentLibraryWindow is null)
{
return;
}
var collapsedChipHost = this.FindControl<Border>("ComponentLibraryCollapsedChipHost");
var collapsedChipTextBlock = this.FindControl<TextBlock>("ComponentLibraryCollapsedChipTextBlock");
var collapsedChipIcon = this.FindControl<Control>("ComponentLibraryCollapsedChipIcon");
if (collapsedChipHost is null || collapsedChipTextBlock is null)
{
return;
}
_componentLibraryCollapsePresenter = new ComponentLibraryCollapsePresenter(
ComponentLibraryWindow,
collapsedChipHost,
collapsedChipTextBlock,
collapsedChipIcon);
}
private bool IsComponentLibraryTemporarilyCollapsedForDesktopEdit()
{
EnsureComponentLibraryCollapsePresenter();
return _componentLibraryCollapsePresenter is not null &&
_componentLibraryCollapsePresenter.VisualState != ComponentLibraryCollapseVisualState.Expanded;
}
private void SyncComponentLibraryCollapseExpandedState()
{
if (!_isComponentLibraryOpen || ComponentLibraryWindow is null)
{
return;
}
EnsureComponentLibraryCollapsePresenter();
if (_componentLibraryCollapsePresenter is null)
{
return;
}
_componentLibraryCollapsePresenter.SyncExpandedState(ComponentLibraryWindow.Margin, ComponentLibraryWindow.Opacity);
}
private void CollapseComponentLibraryForDesktopEdit(string? title)
{
if (!_isComponentLibraryOpen)
{
return;
}
EnsureComponentLibraryCollapsePresenter();
if (_componentLibraryCollapsePresenter is null)
{
return;
}
SyncComponentLibraryCollapseExpandedState();
_componentLibraryCollapsePresenter.Collapse(ResolveComponentLibraryCollapsedChipTitle());
}
private string ResolveComponentLibraryCollapsedChipTitle()
{
if (!string.IsNullOrWhiteSpace(ComponentLibraryTitleTextBlock?.Text))
{
return ComponentLibraryTitleTextBlock.Text;
}
return L("button.component_library", "Widgets");
}
private void RestoreComponentLibraryAfterDesktopEdit()
{
EnsureComponentLibraryCollapsePresenter();
if (_componentLibraryCollapsePresenter is null)
{
return;
}
_componentLibraryCollapsePresenter.Restore();
}
private bool TryGetCurrentDesktopGridGeometry(out DesktopGridGeometry geometry)
{
geometry = default;
if (_currentDesktopCellSize <= 0 ||
_currentDesktopSurfaceIndex < 0 ||
_currentDesktopSurfaceIndex >= _desktopPageCount ||
!_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid))
{
return false;
}
var columnCount = pageGrid.ColumnDefinitions.Count;
var rowCount = pageGrid.RowDefinitions.Count;
if (columnCount <= 0 || rowCount <= 0)
{
return false;
}
geometry = new DesktopGridGeometry(
Origin: default,
CellSize: _currentDesktopCellSize,
CellGap: _currentDesktopCellGap,
ColumnCount: columnCount,
RowCount: rowCount);
return geometry.IsValid;
}
private Rect? GetComponentLibraryBoundsInViewport()
{
if (!_isComponentLibraryOpen ||
IsComponentLibraryTemporarilyCollapsedForDesktopEdit() ||
ComponentLibraryWindow is null ||
DesktopPagesViewport is null ||
!ComponentLibraryWindow.IsVisible ||
ComponentLibraryWindow.Bounds.Width <= 0 ||
ComponentLibraryWindow.Bounds.Height <= 0)
{
return null;
}
var origin = ComponentLibraryWindow.TranslatePoint(default, DesktopPagesViewport);
return origin.HasValue
? new Rect(origin.Value, ComponentLibraryWindow.Bounds.Size)
: null;
}
private static Size GetComponentPixelSize(int widthCells, int heightCells, double cellSize, double cellGap)
{
var safeWidthCells = Math.Max(1, widthCells);
var safeHeightCells = Math.Max(1, heightCells);
return new Size(
safeWidthCells * cellSize + Math.Max(0, safeWidthCells - 1) * cellGap,
safeHeightCells * cellSize + Math.Max(0, safeHeightCells - 1) * cellGap);
}
private string ResolveDesktopEditTitle(string componentId)
{
return _componentRuntimeRegistry.TryGetDescriptor(componentId, out var descriptor)
? descriptor.Definition.DisplayName
: componentId;
}
private void UpdateDesktopEditOverlayMetadata(string componentId, int widthCells, int heightCells, string? detail)
{
EnsureDesktopEditOverlayPresenter();
_desktopEditOverlayPresenter?.UpdateGhostContent(
ResolveDesktopEditTitle(componentId),
detail,
$"{Math.Max(1, widthCells)}x{Math.Max(1, heightCells)}");
}
private bool TryGetDesktopPlacementById(string? placementId, out DesktopComponentPlacementSnapshot placement)
{
placement = null!;
if (string.IsNullOrWhiteSpace(placementId))
{
return false;
}
var matched = _desktopComponentPlacements.FirstOrDefault(candidate =>
string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
if (matched is null)
{
return false;
}
placement = matched;
return true;
}
private void SetDesktopEditSourceHost(Border? host, double opacity)
{
_desktopEditSourceHost = host;
if (_desktopEditSourceHost is not null)
{
_desktopEditSourceHost.Opacity = opacity;
}
}
private void RestoreDesktopEditSourceHost()
{
if (_desktopEditSourceHost is null)
{
return;
}
_desktopEditSourceHost.Opacity = 1;
ApplyDesktopEditStateToHost(_desktopEditSourceHost, _isComponentLibraryOpen);
_desktopEditSourceHost = null;
}
private void ResetDesktopEditState()
{
RestoreDesktopEditSourceHost();
_desktopEditSession = default;
_desktopEditOriginalRect = default;
_desktopEditStartRow = 0;
_desktopEditStartColumn = 0;
_desktopEditStartWidthCells = 0;
_desktopEditStartHeightCells = 0;
_desktopEditMinWidthCells = 0;
_desktopEditMinHeightCells = 0;
_desktopEditMaxWidthCells = 0;
_desktopEditMaxHeightCells = 0;
_desktopEditResizeMode = DesktopComponentResizeMode.Proportional;
_isDesktopEditCommitPending = false;
if (_desktopEditOverlayPresenter is not null)
{
_desktopEditOverlayPresenter.SetCandidateRect(null);
_desktopEditOverlayPresenter.Hide();
}
}
private void CancelDesktopEditSession(bool animate)
{
RestoreComponentLibraryAfterDesktopEdit();
if (_isDesktopEditCommitPending)
{
_desktopEditCommitVersion++;
ResetDesktopEditState();
return;
}
if (!_desktopEditSession.IsActive)
{
ResetDesktopEditState();
return;
}
var version = ++_desktopEditOverlayVersion;
if (animate && _desktopEditOverlayPresenter is not null)
{
_desktopEditOverlayPresenter.Cancel();
DispatcherTimer.RunOnce(
() =>
{
if (version != _desktopEditOverlayVersion)
{
return;
}
ResetDesktopEditState();
},
DesktopEditOverlayAnimationDuration);
return;
}
ResetDesktopEditState();
}
private bool CanCommitDesktopEditAtRect(Rect finalRect)
{
return DesktopPlacementMath.CanCommitPlacement(finalRect, GetComponentLibraryBoundsInViewport());
}
private void RunDesktopEditCommit(Rect finalRect, Action commitAction)
{
_isDesktopEditCommitPending = true;
var overlayVersion = ++_desktopEditOverlayVersion;
var scheduledCommitVersion = ++_desktopEditCommitVersion;
_desktopEditOverlayPresenter?.Commit();
DispatcherTimer.RunOnce(
() =>
{
if (overlayVersion != _desktopEditOverlayVersion ||
!DesktopEditCommitMath.IsPendingCommitValid(
_isDesktopEditCommitPending,
scheduledCommitVersion,
_desktopEditCommitVersion))
{
return;
}
if (!CanCommitDesktopEditAtRect(finalRect))
{
RestoreComponentLibraryAfterDesktopEdit();
ResetDesktopEditState();
return;
}
commitAction();
RestoreComponentLibraryAfterDesktopEdit();
ResetDesktopEditState();
},
DesktopEditOverlayAnimationDuration);
}
private void UpdateDesktopEditSession(Point pointerInViewport)
{
if (_isDesktopEditCommitPending || !_desktopEditSession.IsActive)
{
return;
}
_desktopEditSession = _desktopEditSession
.WithCurrentPointer(pointerInViewport)
.WithComponentLibraryBounds(GetComponentLibraryBoundsInViewport());
switch (_desktopEditSession.Mode)
{
case DesktopEditSessionMode.PendingNew:
PromotePendingNewDesktopEditIfNeeded();
break;
case DesktopEditSessionMode.DraggingNew:
case DesktopEditSessionMode.DraggingExisting:
UpdateActiveDesktopDragPreview();
break;
case DesktopEditSessionMode.ResizingExisting:
UpdateActiveDesktopResizePreview();
break;
}
}
private void PromotePendingNewDesktopEditIfNeeded()
{
var threshold = DesktopPlacementMath.ComputeDragStartThreshold(_currentDesktopCellSize);
if (!_desktopEditSession.HasExceededThreshold(threshold) ||
_desktopEditSession.IsPointerInsideComponentLibrary())
{
return;
}
_desktopEditSession = _desktopEditSession.PromoteToDraggingNew();
CollapseComponentLibraryForDesktopEdit(ResolveDesktopEditTitle(_desktopEditSession.ComponentId ?? string.Empty));
_desktopEditSession = _desktopEditSession.WithComponentLibraryBounds(GetComponentLibraryBoundsInViewport());
EnsureDesktopEditOverlayPresenter();
_desktopEditOverlayPresenter?.Show(DesktopEditGhostVisualStyle.ElevatedFromLibrary);
UpdateActiveDesktopDragPreview();
}
private void UpdateActiveDesktopDragPreview()
{
if (_desktopEditSession.Mode is not (DesktopEditSessionMode.DraggingNew or DesktopEditSessionMode.DraggingExisting) ||
!TryGetCurrentDesktopGridGeometry(out var grid) ||
DesktopPagesViewport is null)
{
return;
}
EnsureDesktopEditOverlayPresenter();
var previewSize = GetComponentPixelSize(
_desktopEditSession.WidthCells,
_desktopEditSession.HeightCells,
_currentDesktopCellSize,
_currentDesktopCellGap);
var previewOrigin = DesktopPlacementMath.Subtract(
_desktopEditSession.CurrentPointerInViewport,
_desktopEditSession.PointerOffsetInViewport);
var previewRect = new Rect(previewOrigin, previewSize);
var hasSnap = DesktopPlacementMath.TryGetSnappedCell(
grid,
_desktopEditSession.CurrentPointerInViewport,
_desktopEditSession.PointerOffsetInViewport,
_desktopEditSession.WidthCells,
_desktopEditSession.HeightCells,
out var column,
out var row);
var snappedRect = hasSnap
? DesktopPlacementMath.GetCellRect(grid, column, row, _desktopEditSession.WidthCells, _desktopEditSession.HeightCells)
: default;
var withinViewport =
_desktopEditSession.CurrentPointerInViewport.X >= 0 &&
_desktopEditSession.CurrentPointerInViewport.Y >= 0 &&
_desktopEditSession.CurrentPointerInViewport.X <= DesktopPagesViewport.Bounds.Width &&
_desktopEditSession.CurrentPointerInViewport.Y <= DesktopPagesViewport.Bounds.Height;
var occludedByLibrary =
_desktopEditSession.IsPointerInsideComponentLibrary() ||
_desktopEditSession.IsPreviewOccludedByComponentLibrary(previewRect);
var canDrop = withinViewport && hasSnap && !occludedByLibrary;
_desktopEditSession = canDrop
? _desktopEditSession.WithTargetCell(row, column)
: _desktopEditSession with { TargetRow = -1, TargetColumn = -1 };
_desktopEditOverlayPresenter?.SetPreviewRect(previewRect);
_desktopEditOverlayPresenter?.SetCandidateRect(canDrop ? snappedRect : null);
_desktopEditOverlayPresenter?.SetInvalid(!canDrop);
}
private void UpdateActiveDesktopResizePreview()
{
if (_desktopEditSession.Mode != DesktopEditSessionMode.ResizingExisting ||
!TryGetCurrentDesktopGridGeometry(out var grid) ||
!TryGetDesktopPlacementById(_desktopEditSession.PlacementId, out var placement))
{
return;
}
EnsureDesktopEditOverlayPresenter();
var deltaX = _desktopEditSession.CurrentPointerInViewport.X - _desktopEditSession.StartPointerInViewport.X;
var deltaY = _desktopEditSession.CurrentPointerInViewport.Y - _desktopEditSession.StartPointerInViewport.Y;
var minSize = GetComponentPixelSize(
_desktopEditMinWidthCells,
_desktopEditMinHeightCells,
_currentDesktopCellSize,
_currentDesktopCellGap);
var maxSize = GetComponentPixelSize(
_desktopEditMaxWidthCells,
_desktopEditMaxHeightCells,
_currentDesktopCellSize,
_currentDesktopCellGap);
double previewWidth;
double previewHeight;
int widthCells;
int heightCells;
if (_desktopEditResizeMode == DesktopComponentResizeMode.Free)
{
previewWidth = Math.Clamp(_desktopEditOriginalRect.Width + deltaX, minSize.Width, maxSize.Width);
previewHeight = Math.Clamp(_desktopEditOriginalRect.Height + deltaY, minSize.Height, maxSize.Height);
widthCells = Math.Clamp(
(int)Math.Round(_desktopEditStartWidthCells + deltaX / CurrentDesktopPitch),
_desktopEditMinWidthCells,
_desktopEditMaxWidthCells);
heightCells = Math.Clamp(
(int)Math.Round(_desktopEditStartHeightCells + deltaY / CurrentDesktopPitch),
_desktopEditMinHeightCells,
_desktopEditMaxHeightCells);
}
else
{
var widthScale = (_desktopEditOriginalRect.Width + deltaX) / Math.Max(1, _desktopEditOriginalRect.Width);
var heightScale = (_desktopEditOriginalRect.Height + deltaY) / Math.Max(1, _desktopEditOriginalRect.Height);
var proposedScale = Math.Max(widthScale, heightScale);
var minScale = Math.Max(
(double)_desktopEditMinWidthCells / Math.Max(1, _desktopEditStartWidthCells),
(double)_desktopEditMinHeightCells / Math.Max(1, _desktopEditStartHeightCells));
var maxScale = Math.Min(
(double)_desktopEditMaxWidthCells / Math.Max(1, _desktopEditStartWidthCells),
(double)_desktopEditMaxHeightCells / Math.Max(1, _desktopEditStartHeightCells));
if (double.IsNaN(proposedScale) || double.IsInfinity(proposedScale))
{
proposedScale = minScale;
}
if (maxScale < minScale)
{
maxScale = minScale;
}
var scale = Math.Clamp(proposedScale, minScale, maxScale);
previewWidth = Math.Clamp(_desktopEditOriginalRect.Width * scale, minSize.Width, maxSize.Width);
previewHeight = Math.Clamp(_desktopEditOriginalRect.Height * scale, minSize.Height, maxSize.Height);
widthCells = Math.Clamp(
(int)Math.Round(_desktopEditStartWidthCells * scale),
_desktopEditMinWidthCells,
_desktopEditMaxWidthCells);
heightCells = Math.Clamp(
(int)Math.Round(_desktopEditStartHeightCells * scale),
_desktopEditMinHeightCells,
_desktopEditMaxHeightCells);
}
var normalized = NormalizeComponentCellSpan(_desktopEditSession.ComponentId ?? string.Empty, (widthCells, heightCells));
widthCells = Math.Clamp(normalized.WidthCells, _desktopEditMinWidthCells, _desktopEditMaxWidthCells);
heightCells = Math.Clamp(normalized.HeightCells, _desktopEditMinHeightCells, _desktopEditMaxHeightCells);
var previewRect = new Rect(_desktopEditOriginalRect.X, _desktopEditOriginalRect.Y, previewWidth, previewHeight);
var snappedRect = DesktopPlacementMath.GetCellRect(grid, placement.Column, placement.Row, widthCells, heightCells);
var occludedByLibrary =
_desktopEditSession.IsPointerInsideComponentLibrary() ||
DesktopPlacementMath.IsOccludedByComponentLibrary(previewRect, _desktopEditSession.ComponentLibraryBounds);
var canCommit = !occludedByLibrary;
_desktopEditSession = (_desktopEditSession with
{
WidthCells = widthCells,
HeightCells = heightCells,
TargetRow = canCommit ? placement.Row : -1,
TargetColumn = canCommit ? placement.Column : -1
}).WithComponentLibraryBounds(GetComponentLibraryBoundsInViewport());
UpdateDesktopEditOverlayMetadata(
_desktopEditSession.ComponentId ?? placement.ComponentId,
widthCells,
heightCells,
L("component.resize", "Resize"));
_desktopEditOverlayPresenter?.SetPreviewRect(previewRect);
_desktopEditOverlayPresenter?.SetCandidateRect(canCommit ? snappedRect : null);
_desktopEditOverlayPresenter?.SetInvalid(!canCommit);
}
private bool CompleteDesktopEditSession(Point pointerInViewport)
{
if (_isDesktopEditCommitPending || !_desktopEditSession.IsActive)
{
return false;
}
UpdateDesktopEditSession(pointerInViewport);
switch (_desktopEditSession.Mode)
{
case DesktopEditSessionMode.DraggingNew:
return CompleteNewDesktopComponentDrag();
case DesktopEditSessionMode.DraggingExisting:
return CompleteExistingDesktopComponentMove();
case DesktopEditSessionMode.ResizingExisting:
return CompleteExistingDesktopComponentResize();
default:
return false;
}
}
private bool CompleteNewDesktopComponentDrag()
{
if (!_desktopEditSession.HasTargetCell ||
string.IsNullOrWhiteSpace(_desktopEditSession.ComponentId) ||
!TryGetCurrentDesktopGridGeometry(out var grid))
{
return false;
}
var finalRect = _desktopEditSession.GetPreviewRect(grid);
if (!CanCommitDesktopEditAtRect(finalRect))
{
return false;
}
_desktopEditOverlayPresenter?.SetPreviewRect(finalRect);
_desktopEditOverlayPresenter?.SetCandidateRect(finalRect);
_desktopEditOverlayPresenter?.SetInvalid(false);
var componentId = _desktopEditSession.ComponentId;
var pageIndex = _desktopEditSession.PageIndex;
var row = _desktopEditSession.TargetRow;
var column = _desktopEditSession.TargetColumn;
RunDesktopEditCommit(finalRect, () => PlaceDesktopComponentOnPage(componentId, pageIndex, row, column));
return true;
}
private bool CompleteExistingDesktopComponentMove()
{
if (!_desktopEditSession.HasTargetCell ||
string.IsNullOrWhiteSpace(_desktopEditSession.PlacementId) ||
!TryGetCurrentDesktopGridGeometry(out var grid))
{
return false;
}
var finalRect = _desktopEditSession.GetPreviewRect(grid);
if (!CanCommitDesktopEditAtRect(finalRect))
{
return false;
}
_desktopEditOverlayPresenter?.SetPreviewRect(finalRect);
_desktopEditOverlayPresenter?.SetCandidateRect(finalRect);
_desktopEditOverlayPresenter?.SetInvalid(false);
var placementId = _desktopEditSession.PlacementId;
var row = _desktopEditSession.TargetRow;
var column = _desktopEditSession.TargetColumn;
if (!DesktopPlacementMath.HasCellPositionChanged(_desktopEditStartRow, _desktopEditStartColumn, row, column))
{
return false;
}
RunDesktopEditCommit(finalRect, () => TryMoveExistingDesktopComponent(placementId, row, column));
return true;
}
private bool CompleteExistingDesktopComponentResize()
{
if (!_desktopEditSession.HasTargetCell ||
string.IsNullOrWhiteSpace(_desktopEditSession.PlacementId) ||
!TryGetCurrentDesktopGridGeometry(out var grid))
{
return false;
}
var finalRect = _desktopEditSession.GetPreviewRect(grid);
if (!CanCommitDesktopEditAtRect(finalRect))
{
return false;
}
_desktopEditOverlayPresenter?.SetPreviewRect(finalRect);
_desktopEditOverlayPresenter?.SetCandidateRect(finalRect);
_desktopEditOverlayPresenter?.SetInvalid(false);
var placementId = _desktopEditSession.PlacementId;
var widthCells = Math.Max(1, _desktopEditSession.WidthCells);
var heightCells = Math.Max(1, _desktopEditSession.HeightCells);
if (!DesktopPlacementMath.HasCellSpanChanged(_desktopEditStartWidthCells, _desktopEditStartHeightCells, widthCells, heightCells))
{
return false;
}
RunDesktopEditCommit(finalRect, () => ApplyExistingDesktopComponentResize(placementId, widthCells, heightCells));
return true;
}
private void ApplyExistingDesktopComponentResize(string placementId, int widthCells, int heightCells)
{
if (!TryGetDesktopPlacementById(placementId, out var placement))
{
return;
}
var before = ClonePlacementSnapshot(placement);
var changed = placement.WidthCells != widthCells || placement.HeightCells != heightCells;
placement.WidthCells = widthCells;
placement.HeightCells = heightCells;
if (_desktopEditSourceHost is not null)
{
Grid.SetColumnSpan(_desktopEditSourceHost, widthCells);
Grid.SetRowSpan(_desktopEditSourceHost, heightCells);
ApplyDesktopEditStateToHost(_desktopEditSourceHost, _isComponentLibraryOpen);
}
if (!changed)
{
return;
}
PersistSettings();
TelemetryServices.Usage?.TrackDesktopComponentResized(before, ClonePlacementSnapshot(placement), "component.resize");
}
}

View File

@@ -200,6 +200,7 @@ public partial class MainWindow
{
DesktopEditDragLayer.Width = pageWidth;
DesktopEditDragLayer.Height = pageHeight;
UpdateDesktopEditOverlayViewportSize();
}
DesktopPagesHost.RowDefinitions.Clear();
@@ -486,8 +487,7 @@ public partial class MainWindow
{
return !_isSettingsOpen &&
!_isComponentLibraryOpen &&
!_isDesktopComponentDragActive &&
!_isDesktopComponentResizeActive &&
!HasActiveDesktopEditSession &&
_desktopSurfacePageWidth > 1;
}

View File

@@ -215,17 +215,21 @@ public partial class MainWindow
string? savedWallpaperPath,
string? type = null,
string? color = null,
string? placement = null)
string? placement = null,
int systemWallpaperRefreshIntervalSeconds = 300)
{
_wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath;
_wallpaperType = string.IsNullOrWhiteSpace(type) ? "Image" : type.Trim();
_wallpaperPlacement = WallpaperImageBrushFactory.NormalizePlacement(placement);
_wallpaperSolidColor = TryParseColor(color, out var parsedColor) ? parsedColor : null;
_wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured;
_systemWallpaperRefreshIntervalSeconds = systemWallpaperRefreshIntervalSeconds;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
StopSystemWallpaperTimer();
if (string.Equals(_wallpaperType, "SolidColor", StringComparison.OrdinalIgnoreCase))
{
_wallpaperMediaType = WallpaperMediaType.SolidColor;
@@ -235,6 +239,14 @@ public partial class MainWindow
return;
}
if (string.Equals(_wallpaperType, "SystemWallpaper", StringComparison.OrdinalIgnoreCase))
{
_wallpaperMediaType = WallpaperMediaType.Image;
LoadSystemWallpaper();
StartSystemWallpaperTimer();
return;
}
if (string.IsNullOrWhiteSpace(_wallpaperPath))
{
_wallpaperMediaType = WallpaperMediaType.None;
@@ -273,6 +285,69 @@ public partial class MainWindow
}
}
private void LoadSystemWallpaper()
{
var systemPath = _systemWallpaperProvider.GetWallpaperPath();
if (string.IsNullOrWhiteSpace(systemPath) || !File.Exists(systemPath))
{
_wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
return;
}
try
{
using var stream = File.OpenRead(systemPath);
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = new Bitmap(stream);
_wallpaperPath = systemPath;
_wallpaperDisplayState = WallpaperDisplayState.CurrentValidWallpaper;
CacheLastValidWallpaperBitmap(systemPath);
}
catch
{
_wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable;
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
}
}
private void StartSystemWallpaperTimer()
{
StopSystemWallpaperTimer();
var intervalSeconds = Math.Clamp(_systemWallpaperRefreshIntervalSeconds, 30, 86400);
_systemWallpaperRefreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(intervalSeconds)
};
_systemWallpaperRefreshTimer.Tick += OnSystemWallpaperRefreshTimerTick;
_systemWallpaperRefreshTimer.Start();
}
private void StopSystemWallpaperTimer()
{
if (_systemWallpaperRefreshTimer is not null)
{
_systemWallpaperRefreshTimer.Stop();
_systemWallpaperRefreshTimer.Tick -= OnSystemWallpaperRefreshTimerTick;
_systemWallpaperRefreshTimer = null;
}
}
private void OnSystemWallpaperRefreshTimerTick(object? sender, EventArgs e)
{
if (!string.Equals(_wallpaperType, "SystemWallpaper", StringComparison.OrdinalIgnoreCase))
{
StopSystemWallpaperTimer();
return;
}
LoadSystemWallpaper();
ApplyWallpaperBrush();
}
private void ApplyWallpaperBrush()
{
DesktopWallpaperImageLayer.Background = null;
@@ -480,7 +555,8 @@ public partial class MainWindow
snapshot.WallpaperPath,
snapshot.WallpaperType,
snapshot.WallpaperColor,
snapshot.WallpaperPlacement);
snapshot.WallpaperPlacement,
snapshot.SystemWallpaperRefreshIntervalSeconds);
if (!snapshot.IsNightMode.HasValue)
{
_isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold;
@@ -523,6 +599,7 @@ public partial class MainWindow
? latestWallpaperState.Color
: null,
WallpaperPlacement = latestWallpaperState.Placement,
SystemWallpaperRefreshIntervalSeconds = latestWallpaperState.SystemWallpaperRefreshIntervalSeconds,
LanguageCode = _languageCode,
TimeZoneId = _timeZoneService.CurrentTimeZone.Id,
WeatherLocationMode = latestWeatherState.LocationMode,

View File

@@ -588,6 +588,44 @@
</Border>
</Grid>
</Border>
<Border x:Name="ComponentLibraryCollapsedChipHost"
IsVisible="False"
IsHitTestVisible="False"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="0,0,0,88"
Padding="14,10"
CornerRadius="999"
Classes="surface-translucent-strong"
BorderBrush="{DynamicResource AdaptiveDockGlassBorderBrush}"
BorderThickness="1"
Opacity="0">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Fast}" />
</Transitions>
</Border.Transitions>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="10">
<fi:FluentIcon x:Name="ComponentLibraryCollapsedChipIcon"
Grid.Column="0"
Icon="Apps"
IconVariant="Regular"
Width="18"
Height="18"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="ComponentLibraryCollapsedChipTextBlock"
Grid.Column="1"
VerticalAlignment="Center"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Widgets" />
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -122,6 +122,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private Color? _wallpaperSolidColor;
private string? _wallpaperPath;
private string _wallpaperStatus = "Current background uses solid color.";
private int _systemWallpaperRefreshIntervalSeconds = 300;
private DispatcherTimer? _systemWallpaperRefreshTimer;
private readonly ISystemWallpaperProvider _systemWallpaperProvider = HostSystemWallpaperProvider.GetOrCreate();
private IReadOnlyList<Color> _recommendedColors = Array.Empty<Color>();
private IReadOnlyList<Color> _monetColors = Array.Empty<Color>();
private Color _selectedThemeColor = Color.Parse("#FF3B82F6");
@@ -160,7 +163,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
public MainWindow()
{
var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService;
var pluginRuntimeService = Design.IsDesignMode
? null
: (Application.Current as App)?.PluginRuntimeService;
_componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService);
_settingsService = _settingsFacade.Settings;
_gridSettingsService = _settingsFacade.Grid;
@@ -173,7 +178,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
InitializeComponent();
Icon = _appLogoService.CreateWindowIcon();
InitializeTaskbarProfileFlyout();
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
_componentRegistry,
pluginRuntimeService,
@@ -183,6 +187,14 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
pluginRuntimeService);
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
_componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade);
if (Design.IsDesignMode)
{
ApplyDesignTimePreview();
return;
}
InitializeTaskbarProfileFlyout();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
_settingsService.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
@@ -196,6 +208,170 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
}
}
private void ApplyDesignTimePreview()
{
Title = "LanMountainDesktop Preview";
ShowInTaskbar = false;
DesktopWallpaperLayer.Background = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops = new GradientStops
{
new GradientStop(Color.Parse("#FFF6F8FB"), 0d),
new GradientStop(Color.Parse("#FFE9EEF7"), 0.55d),
new GradientStop(Color.Parse("#FFDCE5F3"), 1d)
}
};
DesktopWallpaperImageLayer.IsVisible = false;
LauncherPagePanel.IsVisible = false;
ComponentLibraryWindow.IsVisible = false;
BackToWindowsTextBlock.Text = "Back to Windows";
ComponentLibraryTitleTextBlock.Text = "Widgets";
ComponentLibraryBackTextBlock.Text = "Back";
TaskbarProfileDisplayNameTextBlock.Text = "Preview User";
TaskbarProfileSettingsActionTextBlock.Text = "Settings";
TaskbarProfileDesktopEditActionTextBlock.Text = "Edit Desktop";
TaskbarProfileAvatarFallbackText.Text = "P";
TaskbarProfileHeaderAvatarFallbackText.Text = "P";
TaskbarProfileButton.IsEnabled = false;
TaskbarProfilePopup.IsOpen = false;
ClockWidget.IsVisible = true;
ClockWidget.SetDisplayFormat(ClockDisplayFormat.HourMinute);
ClockWidget.SetTransparentBackground(false);
ConfigureDesignTimeDesktopGrid();
PopulateDesignTimeDesktopSurface();
}
private void ConfigureDesignTimeDesktopGrid()
{
const int previewRows = 7;
const int previewColumns = 12;
DesktopGrid.RowDefinitions.Clear();
DesktopGrid.ColumnDefinitions.Clear();
for (var row = 0; row < previewRows; row++)
{
DesktopGrid.RowDefinitions.Add(new RowDefinition(new GridLength(1, GridUnitType.Star)));
}
for (var column = 0; column < previewColumns; column++)
{
DesktopGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
}
DesktopGrid.Margin = new Thickness(28);
DesktopGrid.RowSpacing = 14;
DesktopGrid.ColumnSpacing = 14;
DesktopGrid.Width = double.NaN;
DesktopGrid.Height = double.NaN;
Grid.SetRow(TopStatusBarHost, 0);
Grid.SetColumn(TopStatusBarHost, 0);
Grid.SetRowSpan(TopStatusBarHost, 1);
Grid.SetColumnSpan(TopStatusBarHost, previewColumns);
Grid.SetRow(DesktopPagesViewport, 1);
Grid.SetColumn(DesktopPagesViewport, 0);
Grid.SetRowSpan(DesktopPagesViewport, previewRows - 2);
Grid.SetColumnSpan(DesktopPagesViewport, previewColumns);
Grid.SetRow(BottomTaskbarContainer, previewRows - 1);
Grid.SetColumn(BottomTaskbarContainer, 0);
Grid.SetRowSpan(BottomTaskbarContainer, 1);
Grid.SetColumnSpan(BottomTaskbarContainer, previewColumns);
DesktopPagesHost.ColumnDefinitions.Clear();
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
ClockWidget.ApplyCellSize(72);
}
private void PopulateDesignTimeDesktopSurface()
{
DesktopPagesContainer.Children.Clear();
DesktopPagesContainer.Width = double.NaN;
DesktopPagesContainer.Height = double.NaN;
DesktopPagesContainer.Children.Add(CreateDesignTimePreviewCard(
"Focus Clock",
"Compact widget preview",
32,
32,
300,
170,
"#FFFFFFFF",
"#FFE8EEF8"));
DesktopPagesContainer.Children.Add(CreateDesignTimePreviewCard(
"Weather",
"26°C Qingdao",
360,
86,
260,
132,
"#FFF8FBFF",
"#FFDDE8F6"));
DesktopPagesContainer.Children.Add(CreateDesignTimePreviewCard(
"Study Session",
"Deep work · 48 min",
210,
248,
340,
144,
"#FFFDFEFF",
"#FFE7EEF7"));
}
private static Border CreateDesignTimePreviewCard(
string title,
string subtitle,
double left,
double top,
double width,
double height,
string backgroundColor,
string borderColor)
{
return new Border
{
Width = width,
Height = height,
Margin = new Thickness(left, top, 0, 0),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Background = new SolidColorBrush(Color.Parse(backgroundColor)),
BorderBrush = new SolidColorBrush(Color.Parse(borderColor)),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(28),
Child = new StackPanel
{
Margin = new Thickness(20),
Spacing = 8,
VerticalAlignment = VerticalAlignment.Center,
Children =
{
new TextBlock
{
Text = title,
FontSize = 20,
FontWeight = FontWeight.SemiBold,
Foreground = new SolidColorBrush(Color.Parse("#FF1E293B"))
},
new TextBlock
{
Text = subtitle,
FontSize = 13,
Foreground = new SolidColorBrush(Color.Parse("#FF64748B"))
}
}
}
};
}
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
{
if (sender is not ToggleButton toggleButton)
@@ -231,6 +407,14 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
if (Design.IsDesignMode)
{
ConfigureDesignTimeDesktopGrid();
PopulateDesignTimeDesktopSurface();
return;
}
SyncSettingsWindowState();
_suppressSettingsPersistence = true;
@@ -307,6 +491,12 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
protected override void OnClosed(EventArgs e)
{
if (Design.IsDesignMode)
{
base.OnClosed(e);
return;
}
var wasVisible = IsVisible;
var windowState = WindowState.ToString();

View File

@@ -62,15 +62,34 @@
</ui:InfoBar.IconSource>
</ui:InfoBar>
<Border Classes="settings-section-card">
<StackPanel Spacing="12">
<controls:IconText Icon="WindowConsole"
Text="{Binding RenderBackendLabel}" />
<TextBlock Classes="settings-item-description"
Text="{Binding RenderBackendText}"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- 版权声明 - 放在渲染显示前面 -->
<ui:SettingsExpander Header="版权声明"
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Document" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<WrapPanel>
<WrapPanel.Styles>
<Style Selector="HyperlinkButton">
<Setter Property="Padding" Value="4" />
<Setter Property="Margin" Value="2" />
</Style>
</WrapPanel.Styles>
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop">
<TextBlock Text="GitHub 仓库" />
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://github.com/wwiinnddyy/LanMountainDesktop/issues">
<TextBlock Text="问题反馈" />
</HyperlinkButton>
</WrapPanel>
</ui:SettingsExpanderItem.Footer>
<TextBlock>
<Run Text="Copyright (c) 2024-" /><Run Text="2025" /> Lincube
</TextBlock>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -12,12 +12,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class GeneratedPluginSettingsPage : SettingsPageBase
{
public GeneratedPluginSettingsPage()
: this(
new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US")))
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{
}
@@ -223,4 +218,272 @@ public partial class GeneratedPluginSettingsPage : SettingsPageBase
return textBox;
}
private static PluginGeneratedSettingsPageViewModel CreateDefaultViewModel()
{
return new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US"));
}
private static PluginGeneratedSettingsPageViewModel CreateDesignTimeViewModel()
{
const string pluginId = "preview.plugin";
var settingsService = new DesignTimeSettingsService();
var section = new PluginSettingsSectionRegistration(
"desktop_preview",
"Preview Widget Settings",
[
new SettingsOptionDefinition(
"enable_glow",
SettingsOptionType.Toggle,
"Enable glow",
"Adds a soft highlight around the preview widget.",
true),
new SettingsOptionDefinition(
"refresh_minutes",
SettingsOptionType.Number,
"Refresh interval",
"How often the plugin refreshes its cached content.",
30d,
minimum: 5d,
maximum: 120d),
new SettingsOptionDefinition(
"layout_density",
SettingsOptionType.Select,
"Layout density",
"Choose how compact the widget layout should feel.",
"balanced",
[
new SettingsOptionChoice("compact", "Compact"),
new SettingsOptionChoice("balanced", "Balanced"),
new SettingsOptionChoice("comfortable", "Comfortable")
]),
new SettingsOptionDefinition(
"content_path",
SettingsOptionType.Path,
"Content folder",
"Local folder used by the plugin for mock assets.",
@"C:\Preview\PluginAssets"),
new SettingsOptionDefinition(
"keywords",
SettingsOptionType.List,
"Pinned keywords",
"Comma-separated topics that will be emphasized in the widget.",
new[] { "avalonia", "preview", "design-time" })
],
"Mock plugin settings shown only in Avalonia design mode.");
settingsService.SetValue(
SettingsScope.Plugin,
"enable_glow",
true,
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"refresh_minutes",
30d,
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"layout_density",
"balanced",
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"content_path",
@"C:\Preview\PluginAssets",
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"keywords",
new[] { "avalonia", "preview", "design-time" },
pluginId,
sectionId: section.Id);
return new PluginGeneratedSettingsPageViewModel(
settingsService,
pluginId,
section,
new PluginLocalizer(AppContext.BaseDirectory, "en-US"));
}
private sealed class DesignTimeSettingsService : ISettingsService
{
private readonly Dictionary<string, object?> _values = new(StringComparer.OrdinalIgnoreCase);
public event EventHandler<SettingsChangedEvent>? Changed;
public T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new()
=> new();
public void SaveSnapshot<T>(
SettingsScope scope,
T snapshot,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys);
}
public T LoadSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null) where T : new()
=> new();
public void SaveSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
T section,
string? placementId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys);
}
public void DeleteSection(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null)
{
var prefix = BuildStorageKey(scope, subjectId, placementId, sectionId, key: null);
foreach (var existingKey in _values.Keys.Where(key => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToArray())
{
_values.Remove(existingKey);
}
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys: null);
}
public T? GetValue<T>(
SettingsScope scope,
string key,
string? subjectId = null,
string? placementId = null,
string? sectionId = null)
{
return _values.TryGetValue(BuildStorageKey(scope, subjectId, placementId, sectionId, key), out var value)
? ConvertValue<T>(value)
: default;
}
public void SetValue<T>(
SettingsScope scope,
string key,
T value,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
_values[BuildStorageKey(scope, subjectId, placementId, sectionId, key)] = value;
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys ?? [key]);
}
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
{
return new DesignTimeComponentSettingsAccessor(this, componentId, placementId);
}
private static T? ConvertValue<T>(object? value)
{
if (value is null)
{
return default;
}
if (value is T typedValue)
{
return typedValue;
}
var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
try
{
return (T?)Convert.ChangeType(value, targetType);
}
catch
{
return default;
}
}
private static string BuildStorageKey(
SettingsScope scope,
string? subjectId,
string? placementId,
string? sectionId,
string? key)
{
return string.Join(
"|",
scope,
subjectId ?? string.Empty,
placementId ?? string.Empty,
sectionId ?? string.Empty,
key ?? string.Empty);
}
private void RaiseChanged(
SettingsScope scope,
string? subjectId,
string? placementId,
string? sectionId,
IReadOnlyCollection<string>? changedKeys)
{
Changed?.Invoke(this, new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
}
}
private sealed class DesignTimeComponentSettingsAccessor : IComponentSettingsAccessor
{
private readonly DesignTimeSettingsService _settingsService;
public DesignTimeComponentSettingsAccessor(
DesignTimeSettingsService settingsService,
string componentId,
string? placementId)
{
_settingsService = settingsService;
ComponentId = componentId;
PlacementId = placementId;
}
public string ComponentId { get; }
public string? PlacementId { get; }
public T LoadSnapshot<T>() where T : new()
=> _settingsService.LoadSnapshot<T>(SettingsScope.ComponentInstance, ComponentId, PlacementId);
public void SaveSnapshot<T>(T snapshot, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SaveSnapshot(SettingsScope.ComponentInstance, snapshot, ComponentId, PlacementId, changedKeys: changedKeys);
public T LoadSection<T>(string sectionId) where T : new()
=> _settingsService.LoadSection<T>(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
public void SaveSection<T>(string sectionId, T section, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SaveSection(SettingsScope.ComponentInstance, ComponentId, sectionId, section, PlacementId, changedKeys);
public void DeleteSection(string sectionId)
=> _settingsService.DeleteSection(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
public T? GetValue<T>(string key)
=> _settingsService.GetValue<T>(SettingsScope.ComponentInstance, key, ComponentId, PlacementId);
public void SetValue<T>(string key, T value, IReadOnlyCollection<string>? changedKeys = null)
=> _settingsService.SetValue(SettingsScope.ComponentInstance, key, value, ComponentId, PlacementId, changedKeys: changedKeys);
}
}

View File

@@ -1,3 +1,5 @@
using System;
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
@@ -17,7 +19,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginMarketSettingsPage : SettingsPageBase
{
public PluginMarketSettingsPage()
: this(CreateDefaultViewModel())
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{
}
@@ -34,6 +36,11 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
public override async void OnNavigatedTo(object? parameter)
{
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync();
}
@@ -48,6 +55,113 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
new AirAppMarketReadmeService());
}
private static PluginMarketSettingsPageViewModel CreateDesignTimeViewModel()
{
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
var localizationService = new LocalizationService();
var viewModel = new PluginMarketSettingsPageViewModel(
settingsFacade,
localizationService,
new AirAppMarketIconService(),
new AirAppMarketReadmeService());
var previewHostVersion = new Version(1, 2, 0);
var items = new[]
{
CreateMarketItem(
new PluginMarketPluginInfo(
"news-tiles",
"News Tiles",
"Brings editorial news cards and ticker rows to the desktop.",
"LanMountain Labs",
"1.2.0",
"1.0.0",
"1.0.0",
"https://example.com/news-tiles.zip",
"v1.2.0",
"news-tiles.zip",
string.Empty,
"https://example.com/news-tiles/readme",
"https://example.com/news-tiles",
"https://example.com/news-tiles/repo",
["news", "widgets"],
[],
DateTimeOffset.Now.AddDays(-8),
DateTimeOffset.Now.AddDays(-2)),
localizationService,
installedPlugin: null,
previewHostVersion),
CreateMarketItem(
new PluginMarketPluginInfo(
"workspace-pulse",
"Workspace Pulse",
"Tracks active projects and shows a compact productivity summary.",
"Studio North",
"2.4.0",
"1.0.0",
"1.0.0",
"https://example.com/workspace-pulse.zip",
"v2.4.0",
"workspace-pulse.zip",
string.Empty,
"https://example.com/workspace-pulse/readme",
"https://example.com/workspace-pulse",
"https://example.com/workspace-pulse/repo",
["dashboard", "productivity"],
[],
DateTimeOffset.Now.AddDays(-30),
DateTimeOffset.Now.AddDays(-1)),
localizationService,
new InstalledPluginInfo(
new PluginManifest(
"workspace-pulse",
"Workspace Pulse",
"WorkspacePulse.dll",
"Tracks active projects and shows a compact productivity summary.",
"Studio North",
"2.1.0"),
true,
true,
true,
null),
previewHostVersion),
CreateMarketItem(
new PluginMarketPluginInfo(
"glass-panels",
"Glass Panels",
"Adds experimental acrylic surfaces for plugin-powered widgets.",
"Aster Team",
"0.8.0",
"1.0.0",
"9.0.0",
"https://example.com/glass-panels.zip",
"v0.8.0",
"glass-panels.zip",
string.Empty,
"https://example.com/glass-panels/readme",
"https://example.com/glass-panels",
"https://example.com/glass-panels/repo",
["theme", "experimental"],
[],
DateTimeOffset.Now.AddDays(-12),
DateTimeOffset.Now.AddDays(-3)),
localizationService,
installedPlugin: null,
previewHostVersion)
};
foreach (var item in items)
{
viewModel.MarketPlugins.Add(item);
viewModel.FilteredPlugins.Add(item);
}
viewModel.ShowEmptyState = false;
viewModel.EmptyStateText = string.Empty;
viewModel.StatusMessage = "Showing 3 mocked marketplace plugins for Avalonia design mode.";
return viewModel;
}
private void OnRestartRequested(string? reason)
{
RequestRestart(reason ?? ViewModel.RestartRequiredMessage);
@@ -60,4 +174,17 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
OpenDrawer(drawer, detailViewModel.DrawerTitle);
await detailViewModel.InitializeAsync();
}
private static PluginMarketItemViewModel CreateMarketItem(
PluginMarketPluginInfo plugin,
LocalizationService localizationService,
InstalledPluginInfo? installedPlugin,
Version hostVersion)
{
var languageCode = localizationService.NormalizeLanguageCode(
HostSettingsFacadeProvider.GetOrCreate().Region.Get().LanguageCode);
var item = new PluginMarketItemViewModel(plugin, localizationService, languageCode);
item.ApplyInstallState(installedPlugin, hostVersion);
return item;
}
}

View File

@@ -1,3 +1,4 @@
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
@@ -15,7 +16,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginsSettingsPage : SettingsPageBase
{
public PluginsSettingsPage()
: this(new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
@@ -31,6 +32,11 @@ public partial class PluginsSettingsPage : SettingsPageBase
public override async void OnNavigatedTo(object? parameter)
{
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync();
}
@@ -38,4 +44,47 @@ public partial class PluginsSettingsPage : SettingsPageBase
{
RequestRestart(ViewModel.RestartRequiredMessage);
}
private static PluginsSettingsPageViewModel CreateDesignTimeViewModel()
{
var viewModel = new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate());
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"calendar-plus",
"Calendar Plus",
"CalendarPlus.dll",
"Adds a compact agenda widget and richer date cards.",
"LanMountain Labs",
"1.4.0"),
true,
true,
true,
null)));
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"focus-mode",
"Focus Mode",
"FocusMode.dll",
"Provides a distraction-free overlay and quick toggles.",
"Studio North",
"0.9.2"),
true,
false,
true,
null)));
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"notes-dock",
"Notes Dock",
"NotesDock.dll",
"Pins short markdown notes directly on the desktop.",
"Aster Team",
"2.1.0"),
false,
false,
true,
null)));
viewModel.StatusMessage = "Loaded 3 mocked plugins for Avalonia design mode.";
return viewModel;
}
}

View File

@@ -51,14 +51,9 @@
FontFamily="Consolas"
FontSize="12"
Focusable="False"
IsTabStop="False" />
IsTabStop="False"
HorizontalAlignment="Stretch" />
</StackPanel>
<Button Grid.Column="1"
Content="{Binding RefreshTelemetryIdText}"
Command="{Binding RefreshTelemetryIdCommand}"
VerticalAlignment="Center"
Margin="16,0,0,0"
Classes="accent-button" />
</Grid>
</Border>

View File

@@ -29,6 +29,11 @@
<!-- 纯色预览 -->
<Border Background="{Binding SelectedColor}"
IsVisible="{Binding IsSolidColor}" />
<!-- 系统壁纸预览 -->
<Border Background="#FFF6F7F9"
IsVisible="{Binding IsSystemWallpaper}">
<Border Background="{Binding PreviewBrush}" />
</Border>
</Panel>
</Border>
</Viewbox>
@@ -135,6 +140,19 @@
</Button>
</UniformGrid>
</StackPanel>
<!-- 右侧:系统壁纸状态 -->
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="12" IsVisible="{Binding IsSystemWallpaper}">
<TextBlock Text="{Binding SystemWallpaperLabel}"
FontSize="14"
FontWeight="SemiBold"
Opacity="0.8" />
<TextBlock Text="{Binding SystemWallpaperStatus}"
FontSize="12"
Opacity="0.7"
TextWrapping="Wrap"
MaxWidth="280" />
</StackPanel>
</Grid>
<Separator Classes="settings-separator" Margin="0,0,0,24" />
@@ -183,10 +201,60 @@
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<!-- 填充方式 -->
<!-- 系统壁纸刷新设置 -->
<ui:SettingsExpander Header="{Binding RefreshIntervalLabel}"
IsVisible="{Binding IsSystemWallpaper}"
Margin="0,4,0,0">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Clock" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal" Spacing="8">
<ComboBox Width="140"
ItemsSource="{Binding RefreshIntervals}"
SelectedItem="{Binding SelectedRefreshInterval}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Classes="settings-accent-button"
Command="{Binding RefreshSystemWallpaperCommand}"
ToolTip.Tip="{Binding RefreshButtonTooltip}"
VerticalAlignment="Center"
Padding="12,8">
<fi:SymbolIcon Symbol="ArrowSync" IconVariant="Regular" />
</Button>
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<!-- 填充方式(图片和系统壁纸都显示) -->
<ui:SettingsExpander Header="{Binding WallpaperPlacementLabel}"
Description="{Binding WallpaperPlacementDescription}"
IsVisible="{Binding IsImage}"
IsVisible="{Binding IsImageOrVideo}"
Margin="0,4,0,0">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Maximize" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="200"
ItemsSource="{Binding WallpaperPlacements}"
SelectedItem="{Binding SelectedWallpaperPlacement}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<!-- 系统壁纸填充方式 -->
<ui:SettingsExpander Header="{Binding WallpaperPlacementLabel}"
Description="{Binding WallpaperPlacementDescription}"
IsVisible="{Binding IsSystemWallpaper}"
Margin="0,4,0,0">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Maximize" />

View File

@@ -1,3 +1,4 @@
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
@@ -16,7 +17,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class WeatherSettingsPage : SettingsPageBase
{
public WeatherSettingsPage()
: this(CreateDefaultViewModel())
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{
}
@@ -29,7 +30,7 @@ public partial class WeatherSettingsPage : SettingsPageBase
public WeatherSettingsPageViewModel ViewModel { get; }
private static WeatherSettingsPageViewModel CreateDefaultViewModel()
private static WeatherSettingsPageViewModel CreateDefaultViewModel(bool enableStartupPreviewRefresh = true)
{
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
var localizationService = new LocalizationService();
@@ -42,6 +43,14 @@ public partial class WeatherSettingsPage : SettingsPageBase
settingsFacade,
localizationService,
locationService,
weatherLocationRefreshService);
weatherLocationRefreshService,
enableStartupPreviewRefresh);
}
private static WeatherSettingsPageViewModel CreateDesignTimeViewModel()
{
var viewModel = CreateDefaultViewModel(enableStartupPreviewRefresh: false);
viewModel.ApplyDesignTimePreview();
return viewModel;
}
}

View File

@@ -8,12 +8,12 @@
## 2. 使用场景
| 场景 | 说明 |
|-----|------|
| 学习辅助 | 查看课程表、记录自习时长、获取每日诗词单词 |
| 场景 | 说明 |
| ---- | ---------------------- |
| 学习辅助 | 查看课程表、记录自习时长、获取每日诗词单词 |
| 办公效率 | 查看日历日程、快速访问最近文档、获取新闻资讯 |
| 信息聚合 | 桌面一站式查看天气、日历、热搜、新闻 |
| 个性美化 | 自由定制桌面组件布局、主题、壁纸 |
| 信息聚合 | 桌面一站式查看天气、日历、热搜、新闻 |
| 个性美化 | 自由定制桌面组件布局、主题、壁纸 |
## 3. 解决方案
@@ -27,20 +27,17 @@
## 4. 解决的问题
| 痛点 | 解决方案 |
|-----|---------|
| 信息分散,需打开多个应用 | 桌面聚合展示天气、日历、新闻等信息 |
| 桌面单调,缺乏个性化 | 丰富的组件和主题自由定制 |
| 学习管理不便 | 课程表、自习监测专为学生设计 |
| 痛点 | 解决方案 |
| -------------- | -------------------- |
| 信息分散,需打开多个应用 | 桌面聚合展示天气、日历、新闻等信息 |
| 桌面单调,缺乏个性化 | 丰富的组件和主题自由定制 |
| 学习管理不便 | 课程表、自习监测专为学生设计 |
| 功能单一,需安装多个独立应用 | 一个应用整合考试看板、噪音监测等多种功能 |
| 功能无法满足个性需求 | 插件系统支持无限扩展 |
| 功能无法满足个性需求 | 插件系统支持无限扩展 |
## 5. 产品进度
- **当前版本**v0.7.0(插件 API 3.0.0
- **开发状态**核心功能开发中,预计 v1.0 正式发布
- **开发状态**:功能开发中,预计 1\~2 个月内发布 v1.0 正式
- **用户统计**:通过 PostHog 收集匿名数据(具体数据需后台查看)
---
**一句话总结**:阑山桌面是一款面向个人用户的可定制桌面工具,专注个人学习办公场景,通过组件化设计和插件生态提供轻量、开放、跨平台的桌面信息聚合方案。