diff --git a/.trae/documents/weather-widget-material-redesign.md b/.trae/documents/weather-widget-material-redesign.md new file mode 100644 index 0000000..cad8cfb --- /dev/null +++ b/.trae/documents/weather-widget-material-redesign.md @@ -0,0 +1,559 @@ +# 天气组件 Material Design 重设计计划 + +> **目标:** 全面重构阑山桌面天气组件的视觉设计,遵循 Material Design 3 规范,参考 Google Weather、几何天气、Breez 天气和柠檬天气的设计语言。 + +--- + +## 当前状态分析 + +### 现有组件 +1. **WeatherWidget** - 基础天气(温度+天气状况+位置) +2. **ExtendedWeatherWidget** - 扩展天气(含指标、逐小时、逐日预报) +3. **HourlyWeatherWidget** - 逐小时天气 +4. **MultiDayWeatherWidget** - 多日天气 +5. **WeatherClockWidget** - 天气时钟 + +### 现有问题 +- 排版层次不清晰,文字大小对比不够 +- 布局过于紧凑,缺乏呼吸感 +- 内部卡片使用简单纯色背景,缺乏 Material 风格 +- 背景场景和前景内容缺乏深度分离 +- 圆角和间距不统一 + +### 现有视觉系统 +- 4套调色板:Google(默认)、Geometric、Breezy、LemonFlutter +- 动态背景场景:MaterialWeatherSceneControl 绘制渐变+装饰 +- 图标系统:WeatherIconView + WeatherIconAssetResolver + +--- + +## 设计方向 + +### 核心原则 +1. **Material Design 3** - 使用 M3 的排版、颜色、间距和形状规范 +2. **信息层级清晰** - 大字体温度、次要信息弱化 +3. **呼吸感** - 合理的间距和留白 +4. **深度感** - 前景卡片与背景场景分离 +5. **圆角一致性** - 遵循 DesignCornerRadius 规范 + +### 参考风格 +- **Google Weather** - 大字体温度、清晰层级、圆角卡片、柔和渐变 +- **几何天气** - 几何装饰、现代感 +- **Breez** - 清新留白、柔和色彩 +- **柠檬天气** - 活泼明亮 + +--- + +## 具体改动计划 + +### Task 1: 优化 MaterialWeatherPalette 和调色板系统 + +**文件:** `LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs` + +**改动:** +- 调整所有调色板的对比度,确保文字可读性 +- 优化背景渐变色彩,更加柔和自然 +- 统一文字主色和次色的对比度比例 +- 为每个风格增加 `SurfaceColor` 和 `SurfaceVariantColor` 用于卡片背景 + +**当前调色板字段:** +```csharp +public sealed record MaterialWeatherPalette( + Color BackgroundTop, + Color BackgroundBottom, + Color PrimaryShape, + Color SecondaryShape, + Color AccentShape, + Color TextPrimary, + Color TextSecondary, + Color SurfaceTint, + Color OverlayTint); +``` + +**新增字段:** +```csharp + Color SurfaceColor, // 卡片表面色(低透明度白色/黑色) + Color SurfaceVariantColor, // 变体表面色 + Color OutlineColor // 分割线/边框色 +``` + +--- + +### Task 2: 重构 WeatherWidget(基础天气组件) + +**文件:** +- `LanMountainDesktop/Views/Components/WeatherWidget.axaml` +- `LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs` + +**设计目标:** +- 大字体温度显示(类似 Google Weather) +- 天气状况文字清晰可读 +- 位置和温度范围弱化显示 +- 图标与文字对齐优化 + +**XAML 改动:** +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**CS 改动:** +- 调整响应式布局的字体缩放比例 +- 更新颜色绑定使用新的调色板字段 + +--- + +### Task 3: 重构 ExtendedWeatherWidget(扩展天气组件) + +**文件:** +- `LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml` +- `LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs` + +**设计目标:** +- 顶部区域:位置+温度+图标横向排列 +- 指标区域:使用 Material 3 风格的标签卡片 +- 逐小时预报:水平滚动卡片,时间+图标+温度 +- 逐日预报:列表式布局,日期+图标+高低温 + +**XAML 改动:** +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +**CS 改动:** +- `CreateMetric` 方法:使用圆角卡片,Material 3 风格标签 +- `BuildHourlyItems` 方法:改进卡片样式,统一圆角 +- `BuildDailyItems` 方法:改进卡片样式,统一圆角 + +--- + +### Task 4: 重构 HourlyWeatherWidget(逐小时天气组件) + +**文件:** +- `LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml` +- `LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs` + +**设计目标:** +- 顶部简洁信息栏 +- 逐小时预报使用 Material 卡片风格 +- 时间、图标、温度垂直排列 + +**XAML 改动:** +```xml + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +### Task 5: 重构 MultiDayWeatherWidget(多日天气组件) + +**文件:** +- `LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml` +- `LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs` + +**设计目标:** +- 左侧:当前天气信息(图标+温度+状况+位置) +- 右侧:多日预报列表,使用行式布局 + +**XAML 改动:** +```xml + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +### Task 6: 重构 WeatherClockWidget(天气时钟组件) + +**文件:** +- `LanMountainDesktop/Views/Components/WeatherClockWidget.axaml` +- `LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs` + +**设计目标:** +- 左侧:大字体时间+日期 +- 右侧:天气图标+温度+状况 +- 信息层级清晰 + +**XAML 改动:** +```xml + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +### Task 7: 更新 ExtendedWeatherWidget 的代码后置文件 + +**文件:** `LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs` + +**改动:** +- `CreateMetric` 方法改进: + - 使用 `DesignCornerRadiusSm` 圆角 + - 使用新的 `SurfaceColor` 作为卡片背景 + - 优化字体大小和间距 + +- `BuildHourlyItems` 方法改进: + - 使用 `DesignCornerRadiusSm` 圆角 + - 使用 `SurfaceColor` 作为卡片背景 + - 时间、图标、温度垂直排列,居中对齐 + +- `BuildDailyItems` 方法改进: + - 使用 `DesignCornerRadiusSm` 圆角 + - 使用 `SurfaceColor` 作为卡片背景 + - 日期、图标、高低温垂直排列 + +--- + +### Task 8: 更新 HourlyWeatherWidget 的代码后置文件 + +**文件:** `LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs` + +**改动:** +- `CreateChip` 方法改进: + - 使用 `DesignCornerRadiusSm` 圆角 + - 使用 `SurfaceColor` 作为卡片背景 + - 优化垂直排列的间距 + +--- + +### Task 9: 更新 MultiDayWeatherWidget 的代码后置文件 + +**文件:** `LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs` + +**改动:** +- `CreateRow` 方法改进: + - 添加底部分割线(除最后一行) + - 优化列间距和对齐 + - 高低温使用不同透明度区分 + +--- + +### Task 10: 更新 MaterialWeatherVisualTheme 调色板 + +**文件:** `LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs` + +**改动:** +- 为 `MaterialWeatherPalette` 添加新字段: + - `SurfaceColor` - 用于卡片表面 + - `SurfaceVariantColor` - 用于变体表面 + - `OutlineColor` - 用于分割线 + +- 更新所有调色板生成方法: + - `ResolveGooglePalette` + - `ResolveGeometricPalette` + - `ResolveBreezyPalette` + - `ResolveLemonPalette` + +- 每个调色板需要为白天/夜晚模式提供合适的 SurfaceColor: + - 白天:低透明度白色(如 `#14FFFFFF`) + - 夜晚:低透明度黑色(如 `#1A000000`) + +--- + +### Task 11: 构建和测试 + +**命令:** +```bash +dotnet build LanMountainDesktop.slnx -c Debug +dotnet test LanMountainDesktop.slnx -c Debug +``` + +**验证清单:** +- [ ] 所有天气组件正常编译 +- [ ] 运行时无异常 +- [ ] 4套视觉风格正常切换 +- [ ] 响应式布局正常工作 +- [ ] 圆角资源正确应用 + +--- + +## 文件改动汇总 + +| 文件 | 改动类型 | 说明 | +|------|---------|------| +| `MaterialWeatherVisualTheme.cs` | 修改 | 添加 SurfaceColor 等字段,更新所有调色板 | +| `WeatherWidget.axaml` | 修改 | 重构布局,优化排版 | +| `WeatherWidget.axaml.cs` | 修改 | 调整响应式布局和颜色绑定 | +| `ExtendedWeatherWidget.axaml` | 修改 | 重构布局,添加卡片容器 | +| `ExtendedWeatherWidget.axaml.cs` | 修改 | 改进卡片创建方法 | +| `HourlyWeatherWidget.axaml` | 修改 | 重构布局,添加卡片容器 | +| `HourlyWeatherWidget.axaml.cs` | 修改 | 改进卡片创建方法 | +| `MultiDayWeatherWidget.axaml` | 修改 | 重构布局,添加卡片容器 | +| `MultiDayWeatherWidget.axaml.cs` | 修改 | 改进行创建方法 | +| `WeatherClockWidget.axaml` | 修改 | 重构布局,优化排版 | +| `WeatherClockWidget.axaml.cs` | 修改 | 调整响应式布局 | + +--- + +## 设计规范检查清单 + +- [ ] 所有组件根容器使用 `DesignCornerRadiusComponent` +- [ ] 内部卡片使用 `DesignCornerRadiusMd` 或 `DesignCornerRadiusSm` +- [ ] 不使用硬编码圆角值 +- [ ] 文字对比度符合 VISUAL_SPEC 要求 +- [ ] 间距使用一致的倍数(4px 基线) +- [ ] 字体层级:温度(64-72px) > 状况(16-18px) > 位置/范围(12-13px) diff --git a/.trae/documents/weather-widget-visual-redesign.md b/.trae/documents/weather-widget-visual-redesign.md new file mode 100644 index 0000000..3949b5e --- /dev/null +++ b/.trae/documents/weather-widget-visual-redesign.md @@ -0,0 +1,342 @@ +# 天气组件视觉重构 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 彻底重构阑山桌面天气系列组件的背景视觉和文字排版,为每种图标风格(Google Weather / Geometric / Breezy / Lemon)提供独立的背景配色和视觉质感,参考各天气 App 的 Material Design 风格,实现几何质感+柔和渐变+层次分明的排版。 + +**Architecture:** 保留现有数据层(WeatherWidgetBase、WeatherSnapshot、WeatherIconAssetResolver)和组件注册机制不变。核心改动:1) 将 `MaterialWeatherVisualTheme.ResolvePalette()` 扩展为按 styleId 分派不同配色方案;2) 重构 `MaterialWeatherSceneControl` 为按 styleId 渲染不同背景风格;3) 改进各天气 Widget 的文字排版层次。先创建 HTML Mock 验证视觉效果。 + +**Tech Stack:** Avalonia UI (XAML + C# code-behind)、HTML/CSS (Mock 预览) + +--- + +## 当前状态分析 + +### 现有天气组件体系 +5 个天气组件,全部继承自 `WeatherWidgetBase`: + +| 组件 | 文件 | 功能 | +|------|------|------| +| WeatherWidget | `WeatherWidget.axaml(.cs)` | 基础天气:温度+状况+图标+位置 | +| WeatherClockWidget | `WeatherClockWidget.axaml(.cs)` | 天气+时钟 | +| ExtendedWeatherWidget | `ExtendedWeatherWidget.axaml(.cs)` | 扩展天气:含指标/小时/多日预报 | +| HourlyWeatherWidget | `HourlyWeatherWidget.axaml(.cs)` | 逐小时天气 | +| MultiDayWeatherWidget | `MultiDayWeatherWidget.axaml(.cs)` | 多日天气 | + +### 核心问题 + +1. **背景与图标风格脱钩**: `MaterialWeatherVisualTheme.ResolvePalette()` 只返回一套配色,与 `WeatherVisualStyleId`(GoogleWeatherV4/Geometric/Breezy/LemonFlutter)完全无关。切换图标风格时背景不变。 +2. **背景视觉单调**: `MaterialWeatherSceneControl` 只有一种手绘几何风格(椭圆+云+雨滴),质感差,缺乏各 App 的特色。 +3. **文字排版粗糙**: 温度数字不够大,信息层次不分明,指标用纯文字堆叠,预报区域无卡片样式。 +4. **半透明遮罩硬编码**: 所有组件都覆盖 `` 等硬编码遮罩,不随风格变化。 + +### 各天气 App 风格特征 + +**Google Weather (v4)**: +- 背景:大面积柔和蓝白渐变,晴天偏暖黄蓝,雨天偏深蓝灰 +- 装饰:极简,几乎无几何装饰,纯靠渐变色彩表现天气氛围 +- 排版:温度超大(72px+),天气状况中等,位置小字 + +**Geometric Weather (几何天气)**: +- 背景:深色系渐变(深蓝/深紫/深灰),搭配半透明几何圆形装饰 +- 装饰:大面积半透明圆形叠加,营造深度感 +- 排版:紧凑信息密度,指标用小标签 + +**Breezy Weather (微风天气)**: +- 背景:清新渐变(浅蓝/浅绿/浅紫),比 Geometric 更明亮 +- 装饰:柔和波浪线条 + 少量几何装饰,Material Design 风格 +- 排版:卡片式预报,圆角芯片 + +**Lemon Weather (柠檬天气)**: +- 背景:暖色系渐变(橙黄/粉紫/暖蓝),柠檬2偏扁平,柠檬3偏Material +- 装饰:天气场景装饰(太阳光芒/云朵轮廓/雨丝),更有场景感 +- 排版:温度超大,天气图标突出 + +--- + +## 设计方案 + +### 视觉论文 (Visual Thesis) +每种图标风格拥有独特的背景渐变配色和几何装饰语言——Google 纯净渐变、Geometric 深色几何、Breezy 清新波浪、Lemon 暖色场景——配合超大温度数字和层次分明的排版,在桌面小组件空间内实现 Material Design 的几何质感。 + +### 配色方案设计 + +每种风格 × 每种天气条件 × 昼夜 = 独立配色。以下为关键配色定义: + +#### Google Weather 风格 +| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom | +|------|----------------|----------------| +| Clear | #4FC3F7 → #B3E5FC | #0D47A1 → #1A237E | +| PartlyCloudy | #81D4FA → #E1F5FE | #1565C0 → #283593 | +| Cloudy | #90A4AE → #CFD8DC | #37474F → #455A64 | +| Rain | #78909C → #B0BEC5 | #263238 → #37474F | +| Storm | #546E7A → #78909C | #1A1A2E → #263238 | +| Snow | #E1F5FE → #FFFFFF | #1A237E → #283593 | +| Fog/Haze | #B0BEC5 → #ECEFF1 | #455A64 → #546E7A | + +#### Geometric 风格 +| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom | +|------|----------------|----------------| +| Clear | #1A237E → #3949AB | #0A0E27 → #1A1A3E | +| PartlyCloudy | #283593 → #5C6BC0 | #0D1033 → #1E1E4A | +| Cloudy | #37474F → #607D8B | #1A1A2E → #2D2D44 | +| Rain | #1A237E → #3F51B5 | #0A0E27 → #1A1A3E | +| Storm | #1A1A2E → #3F51B5 | #050510 → #1A1A2E | +| Snow | #E8EAF6 → #C5CAE9 | #1A237E → #283593 | +| Fog/Haze | #455A64 → #78909C | #1A1A2E → #37474F | + +#### Breezy 风格 +| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom | +|------|----------------|----------------| +| Clear | #4DD0E1 → #80DEEA | #006064 → #00838F | +| PartlyCloudy | #4FC3F7 → #B2EBF2 | #00695C → #00897B | +| Cloudy | #80CBC4 → #B2DFDB | #37474F → #546E7A | +| Rain | #4DB6AC → #80CBC4 | #004D40 → #00695C | +| Storm | #26A69A → #4DB6AC | #1A1A2E → #004D40 | +| Snow | #E0F7FA → #FFFFFF | #006064 → #00838F | +| Fog/Haze | #80CBC4 → #E0F7FA | #37474F → #546E7A | + +#### Lemon 风格 +| 天气 | 白天 Top→Bottom | 夜晚 Top→Bottom | +|------|----------------|----------------| +| Clear | #FFB74D → #FFF176 | #1A237E → #311B92 | +| PartlyCloudy | #FF8A65 → #FFCC80 | #283593 → #4A148C | +| Cloudy | #BCAAA4 → #D7CCC8 | #37474F → #4E342E | +| Rain | #90A4AE → #B0BEC5 | #1A1A2E → #311B92 | +| Storm | #78909C → #90A4AE | #0D0D1A → #1A1A2E | +| Snow | #FFF9C4 → #FFFFFF | #1A237E → #311B92 | +| Fog/Haze | #D7CCC8 → #EFEBE9 | #4E342E → #5D4037 | + +### 排版改进方案 + +1. **温度超大化**: 温度字号从 56-58px 提升到 64-72px(基础组件),形成视觉锚点 +2. **层次分明**: 温度 → 天气状况 → 位置/指标,字号递减,透明度递减 +3. **指标标签化**: 湿度/风速/AQI 用半透明圆角标签展示,而非纯文字 +4. **预报芯片化**: 小时/每日预报用圆角半透明芯片卡片 +5. **图标间距**: 天气图标与文字之间增加 8-12px 间距 + +--- + +## 文件变更清单 + +| 文件 | 操作 | 说明 | +|------|------|------| +| `Views/Components/MaterialWeatherVisualTheme.cs` | 修改 | 扩展 ResolvePalette 支持 styleId 分派,新增4套风格配色 | +| `Views/Components/MaterialWeatherSceneControl.cs` | 修改 | 按 styleId 渲染不同背景风格(纯渐变/深色几何/清新波浪/暖色场景) | +| `Views/Components/WeatherWidgetBase.cs` | 修改 | 传递 styleId 到 SceneControl.Apply(),移除硬编码遮罩 | +| `Views/Components/WeatherWidget.axaml` | 修改 | 改进排版层次,移除硬编码遮罩 | +| `Views/Components/WeatherWidget.axaml.cs` | 修改 | 适配新排版 | +| `Views/Components/WeatherClockWidget.axaml` | 修改 | 改进排版,移除硬编码遮罩 | +| `Views/Components/WeatherClockWidget.axaml.cs` | 修改 | 适配新排版 | +| `Views/Components/ExtendedWeatherWidget.axaml` | 修改 | 改进排版,指标标签化,预报芯片化 | +| `Views/Components/ExtendedWeatherWidget.axaml.cs` | 修改 | 适配新排版+标签+芯片 | +| `Views/Components/HourlyWeatherWidget.axaml` | 修改 | 改进排版,预报芯片化 | +| `Views/Components/HourlyWeatherWidget.axaml.cs` | 修改 | 适配新排版+芯片 | +| `Views/Components/MultiDayWeatherWidget.axaml` | 修改 | 改进排版 | +| `Views/Components/MultiDayWeatherWidget.axaml.cs` | 修改 | 适配新排版 | +| `mocks/weather-widget-mock.html` | 新建 | HTML Mock 预览(4种风格×2种天气×2种主题) | + +--- + +## Task 分解 + +### Task 1: 创建 HTML Mock 预览 + +**Files:** +- Create: `mocks/weather-widget-mock.html` + +- [ ] **Step 1: 创建 HTML Mock 文件** + +创建完整的 HTML Mock,包含: +- 4 种风格(Google / Geometric / Breezy / Lemon)× 2 种天气(晴/雨)× 2 种主题(亮/暗) +- 每种风格展示基础天气组件(温度+状况+图标+位置) +- 改进后的排版:超大温度、层次分明、指标标签化 +- 亮色/暗色主题切换按钮 + +- [ ] **Step 2: 在浏览器中打开 Mock 验证效果** + +Run: `start mocks/weather-widget-mock.html` + +--- + +### Task 2: 扩展 MaterialWeatherVisualTheme 支持多风格配色 + +**Files:** +- Modify: `LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs` + +- [ ] **Step 1: 修改 ResolvePalette 方法签名** + +将 `ResolvePalette(MaterialWeatherCondition condition, bool isNight)` 改为 `ResolvePalette(string? styleId, MaterialWeatherCondition condition, bool isNight)`,内部按 styleId 分派到不同配色方案。 + +- [ ] **Step 2: 新增 Google Weather 配色表** + +为 GoogleWeatherV4 风格定义所有天气条件×昼夜的配色(参考上面配色方案设计章节)。 + +- [ ] **Step 3: 新增 Geometric 配色表** + +为 Geometric 风格定义深色系配色。 + +- [ ] **Step 4: 新增 Breezy 配色表** + +为 Breezy 风格定义清新渐变配色。 + +- [ ] **Step 5: 新增 Lemon 配色表** + +为 LemonFlutter 风格定义暖色系配色。 + +- [ ] **Step 6: 更新所有调用点** + +将所有 `ResolvePalette(condition, isNight)` 调用改为 `ResolvePalette(styleId, condition, isNight)`。 + +--- + +### Task 3: 重构 MaterialWeatherSceneControl 支持多风格背景 + +**Files:** +- Modify: `LanMountainDesktop/Views/Components/MaterialWeatherSceneControl.cs` + +- [ ] **Step 1: 扩展 Apply 方法签名** + +将 `Apply(MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive)` 改为 `Apply(string? styleId, MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive)`,存储 styleId。 + +- [ ] **Step 2: 实现 Google Weather 风格渲染** + +纯渐变背景,无几何装饰。背景使用 palette 的 BackgroundTop→BackgroundBottom 渐变。仅保留天气特效(雨滴/雪花/雾线)。 + +- [ ] **Step 3: 实现 Geometric 风格渲染** + +深色渐变 + 大面积半透明几何圆形叠加。在基础渐变上叠加 2-3 个大椭圆(使用 palette 的 PrimaryShape/SecondaryShape/AccentShape),营造深度感。保留天气特效。 + +- [ ] **Step 4: 实现 Breezy 风格渲染** + +清新渐变 + 柔和波浪线条。在基础渐变上绘制 2-3 条正弦波浪线(使用 palette 的 SurfaceTint),营造微风感。保留天气特效。 + +- [ ] **Step 5: 实现 Lemon 风格渲染** + +暖色渐变 + 天气场景装饰。晴天绘制太阳光芒(放射线),多云绘制云朵轮廓,雨天绘制雨丝。保留天气特效。 + +- [ ] **Step 6: 更新所有调用点** + +将所有 `SceneControl.Apply(condition, palette, isLive)` 改为 `SceneControl.Apply(styleId, condition, palette, isLive)`。 + +--- + +### Task 4: 更新 WeatherWidgetBase 传递 styleId + +**Files:** +- Modify: `LanMountainDesktop/Views/Components/WeatherWidgetBase.cs` + +- [ ] **Step 1: 修改 ApplyCurrentScene 方法** + +在 `ApplyCurrentScene()` 中将 `CurrentVisualStyleId` 传递给 `SceneControl.Apply()`。 + +- [ ] **Step 2: 修改 ApplySnapshot 中的 ResolvePalette 调用** + +将 `MaterialWeatherVisualTheme.ResolvePalette(CurrentCondition, isNight)` 改为 `MaterialWeatherVisualTheme.ResolvePalette(CurrentVisualStyleId, CurrentCondition, isNight)`。 + +--- + +### Task 5: 改进各天气 Widget 的 XAML 排版 + +**Files:** +- Modify: `WeatherWidget.axaml` — 移除硬编码遮罩 ``,改用 palette 驱动的半透明遮罩 +- Modify: `WeatherClockWidget.axaml` — 同上 +- Modify: `ExtendedWeatherWidget.axaml` — 同上 + 指标区域改用标签样式 +- Modify: `HourlyWeatherWidget.axaml` — 同上 + 预报区域改用芯片样式 +- Modify: `MultiDayWeatherWidget.axaml` — 同上 + +- [ ] **Step 1: 移除所有硬编码遮罩** + +将 `` / `#42FFFFFF` / `#34FFFFFF` / `#38FFFFFF` / `#3CFFFFFF` 替换为 ``,在 code-behind 中根据 palette 设置遮罩颜色。 + +- [ ] **Step 2: 改进 WeatherWidget 排版** + +增大温度字号(58→64),增加图标与文字间距,调整位置文字透明度。 + +- [ ] **Step 3: 改进 WeatherClockWidget 排版** + +增大时钟字号,增加天气信息与时间间距。 + +- [ ] **Step 4: 改进 ExtendedWeatherWidget 排版** + +指标用半透明圆角标签,小时/每日预报用圆角芯片卡片。 + +- [ ] **Step 5: 改进 HourlyWeatherWidget 排版** + +预报区域用圆角芯片卡片样式。 + +- [ ] **Step 6: 改进 MultiDayWeatherWidget 排版** + +每日预报行增加分隔线和更好的间距。 + +--- + +### Task 6: 更新各天气 Widget 的 code-behind + +**Files:** +- Modify: 所有天气 Widget 的 `.axaml.cs` 文件 + +- [ ] **Step 1: 更新 WeatherWidget.axaml.cs** + +- 设置 OverlayBorder 背景 +- 增大温度字号 +- 适配新排版参数 + +- [ ] **Step 2: 更新 WeatherClockWidget.axaml.cs** + +- 设置 OverlayBorder 背景 +- 适配新排版 + +- [ ] **Step 3: 更新 ExtendedWeatherWidget.axaml.cs** + +- 设置 OverlayBorder 背景 +- 指标标签化(CreateMetric 改为带圆角背景的标签) +- 预报芯片化 + +- [ ] **Step 4: 更新 HourlyWeatherWidget.axaml.cs** + +- 设置 OverlayBorder 背景 +- 预报芯片化(CreateChip 改为带圆角背景的芯片) + +- [ ] **Step 5: 更新 MultiDayWeatherWidget.axaml.cs** + +- 设置 OverlayBorder 背景 +- 适配新排版 + +--- + +### Task 7: 验证与测试 + +- [ ] **Step 1: 运行项目查看效果** + +Run: `dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj` + +- [ ] **Step 2: 运行相关测试** + +Run: `dotnet test LanMountainDesktop.slnx -c Debug` + +- [ ] **Step 3: 检查圆角规范合规** + +确认所有组件 RootBorder 使用 `DesignCornerRadiusComponent`,新增的标签/芯片使用 `DesignCornerRadiusSm`/`DesignCornerRadiusMd`。 + +--- + +## 假设与决策 + +1. **4 套独立风格**: 每种图标风格对应独立的背景配色和装饰风格,切换图标风格时背景也跟着变 +2. **配色表驱动**: 所有颜色定义在 `MaterialWeatherVisualTheme` 中,不硬编码到 SceneControl +3. **保留天气特效**: 雨滴/雪花/雾线/闪电等天气特效在所有风格中保留,但颜色跟随 palette +4. **遮罩动态化**: 半透明遮罩颜色从 palette 中派生,而非硬编码 `#30FFFFFF` +5. **排版渐进改进**: 不做大规模 XAML 重构,而是在现有结构上优化字号/间距/透明度 +6. **数据层不变**: WeatherSnapshot、WeatherIconAssetResolver、WeatherWidgetBase 的数据逻辑不变 +7. **接口兼容**: IDesktopComponentWidget 等接口实现不变 + +## 验证步骤 + +1. HTML Mock 在浏览器中展示 4 种风格效果满意 +2. Avalonia 项目编译通过 +3. 运行项目,切换图标风格时背景配色和装饰风格跟着变化 +4. 亮色/暗色主题切换正常 +5. 5 个天气组件排版层次分明 +6. 指标标签化和预报芯片化正常显示 +7. 测试通过 diff --git a/.trae/specs/dock-back-to-windows-button-display/checklist.md b/.trae/specs/dock-back-to-windows-button-display/checklist.md new file mode 100644 index 0000000..ebf0896 --- /dev/null +++ b/.trae/specs/dock-back-to-windows-button-display/checklist.md @@ -0,0 +1,13 @@ +# Checklist + +- [ ] `AppSettingsSnapshot.BackToWindowsButtonDisplayMode` exists and defaults to `IconAndText`. +- [ ] `AppSettingsSnapshot` contains icon source, Fluent icon name, and text icon settings with safe defaults. +- [ ] General > Basic Settings includes one folded back-to-platform button settings expander. +- [ ] The expander includes the display-mode dropdown. +- [ ] The expander includes nested icon source, Fluent icon popup picker, and text icon input controls. +- [ ] The Dock button left icon slot renders either a Fluent icon or custom text. +- [ ] `IconAndText`, `IconOnly`, and `TextOnly` modes update the Dock button live. +- [ ] Icon source, Fluent icon name, and text icon updates refresh the Dock button live. +- [ ] The selected mode is preserved when MainWindow saves app settings. +- [ ] Localization keys exist for zh-CN, en-US, ja-JP, and ko-KR. +- [ ] `dotnet build LanMountainDesktop.slnx -c Debug` succeeds. diff --git a/.trae/specs/dock-back-to-windows-button-display/spec.md b/.trae/specs/dock-back-to-windows-button-display/spec.md new file mode 100644 index 0000000..c0d108b --- /dev/null +++ b/.trae/specs/dock-back-to-windows-button-display/spec.md @@ -0,0 +1,29 @@ +# Dock Back To Windows Button Display + +## Summary + +The Dock "Back to platform" action should expose a configurable left icon slot while keeping the localized platform text fixed. + +## Requirements + +- The default display mode is `IconAndText` so existing users keep a familiar Dock layout after upgrade. +- The localized platform text remains controlled by the app and is not user-editable. +- General > Basic Settings exposes one Fluent Avalonia `FASettingsExpander` for the back-to-platform button, with icon-related controls folded into nested `FASettingsExpanderItem` rows. +- The main row exposes a dropdown with `IconAndText`, `IconOnly`, and `TextOnly` options. +- A nested icon source row selects Fluent icon or text icon. +- Fluent icon mode uses a popup picker-style flyout with search and a grid of the full FluentIcons `Icon` enum. +- Text icon mode lets the user enter short text for the left icon slot. +- Changing the dropdown persists to `AppSettingsSnapshot.BackToWindowsButtonDisplayMode` and updates the Dock button without restarting. +- Changing the icon source, Fluent icon, or text icon persists to app settings and updates the Dock button without restarting. +- `IconOnly` keeps the existing tooltip text so the button remains understandable. +- `PinnedTaskbarActions` continues to control whether the action is visible; it does not replace the display mode setting. + +## Acceptance Scenarios + +- With default settings, the Dock button shows a small circle icon and the localized platform text. +- Selecting icon only hides the platform text and keeps the configured left icon visible. +- Selecting text only hides the left icon slot and keeps the localized platform text visible. +- Choosing a Fluent icon changes the left icon slot. +- Entering a short text icon changes the left icon slot. +- Restarting the app restores the selected display mode. +- Clicking the button still runs the existing minimize/back-to-platform behavior. diff --git a/LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs b/LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs index 7d7a23b..b0e6c11 100644 --- a/LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs +++ b/LanMountainDesktop.Tests/DesktopComponentRenderModeTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Linq; using Avalonia.Controls; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; @@ -73,6 +75,48 @@ public sealed class DesktopComponentRenderModeTests Assert.Null(probe.RuntimeContext?.PlacementId); } + [Fact] + public void DefaultRuntimeRegistrations_IncludeMaterialWeatherComponents() + { + var componentIds = DesktopComponentRuntimeRegistry.GetDefaultRegistrations() + .Select(registration => registration.ComponentId) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + Assert.Contains(BuiltInComponentIds.DesktopWeatherClock, componentIds); + Assert.Contains(BuiltInComponentIds.DesktopWeather, componentIds); + Assert.Contains(BuiltInComponentIds.DesktopHourlyWeather, componentIds); + Assert.Contains(BuiltInComponentIds.DesktopMultiDayWeather, componentIds); + Assert.Contains(BuiltInComponentIds.DesktopExtendedWeather, componentIds); + } + + [Fact] + public void WeatherVisualStyleCatalog_NormalizesLegacyAndSupportedIds() + { + Assert.Equal(WeatherVisualStyleId.GoogleWeatherV4, WeatherVisualStyleCatalog.Normalize(null)); + Assert.Equal(WeatherVisualStyleId.GoogleWeatherV4, WeatherVisualStyleCatalog.Normalize("DefaultWeather")); + Assert.Equal(WeatherVisualStyleId.GoogleWeatherV4, WeatherVisualStyleCatalog.Normalize("HyperOS3")); + Assert.Equal(WeatherVisualStyleId.Geometric, WeatherVisualStyleCatalog.Normalize("Geometric")); + Assert.Equal(WeatherVisualStyleId.Breezy, WeatherVisualStyleCatalog.Normalize("Breezy")); + Assert.Equal(WeatherVisualStyleId.LemonFlutter, WeatherVisualStyleCatalog.Normalize("LemonFlutter")); + Assert.Equal(WeatherVisualStyleId.GoogleWeatherV4, WeatherVisualStyleCatalog.Normalize("MissingPack")); + } + + [Theory] + [InlineData(WeatherVisualStyleId.GoogleWeatherV4)] + [InlineData(WeatherVisualStyleId.Geometric)] + [InlineData(WeatherVisualStyleId.Breezy)] + [InlineData(WeatherVisualStyleId.LemonFlutter)] + public void WeatherIconAssetResolver_ResolvesCoreWeatherStates(string styleId) + { + Assert.NotNull(WeatherIconAssetResolver.ResolveAssetUri(styleId, 0, "Clear", isDaylight: true)); + Assert.NotNull(WeatherIconAssetResolver.ResolveAssetUri(styleId, 1, "Partly cloudy", isDaylight: false)); + Assert.NotNull(WeatherIconAssetResolver.ResolveAssetUri(styleId, 7, "Rain", isDaylight: true)); + Assert.NotNull(WeatherIconAssetResolver.ResolveAssetUri(styleId, 4, "Thunderstorm", isDaylight: true)); + Assert.NotNull(WeatherIconAssetResolver.ResolveAssetUri(styleId, 13, "Snow", isDaylight: true)); + Assert.NotNull(WeatherIconAssetResolver.ResolveAssetUri(styleId, 18, "Fog", isDaylight: true)); + Assert.NotNull(WeatherIconAssetResolver.ResolveAssetUri(styleId, 999, "Unknown", isDaylight: true)); + } + private static DesktopComponentRuntimeDescriptor CreateDescriptor() { Assert.True(CreateRuntimeRegistry().TryGetDescriptor(ComponentId, out var descriptor)); diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/NOTICE.md b/LanMountainDesktop/Assets/MaterialWeatherIcons/NOTICE.md new file mode 100644 index 0000000..525c2f8 --- /dev/null +++ b/LanMountainDesktop/Assets/MaterialWeatherIcons/NOTICE.md @@ -0,0 +1,16 @@ +# Material Weather Icons Sources + +This folder contains weather icon assets imported for LanMountainDesktop weather widgets. + +The icon packs are included because the product owner explicitly requested original-style assets and accepted the licensing risk for uncertain or non-free icon packs. Keep this notice with the assets. + +## Included packs + +- `google-weather-v4`: imported from `mbatthew/GoogleWeatherIconsV4Pack`, which documents that the application code is MIT but the weather icons are sourced from Google Weather Icons v4 and have uncertain licensing. +- `geometric`: imported from `breezy-weather/geometric-icon-provider`, an open-source rewrite of the Geometric Weather icon provider. Breezy's icon-pack index documents related assets as non-free. +- `breezy`: imported from `breezy-weather/breezy-weather` app resources. Breezy Weather is LGPL-3.0; verify individual asset terms before external redistribution. +- `lemon-flutter`: imported from `yangSpica27/spica_weather_flutter`, MIT licensed. + +## Pending sources + +- Lemon Weather v2/v3 are not included yet. They should only be added after a concrete public repository and asset license are verified. diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/SOURCE.md b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/SOURCE.md new file mode 100644 index 0000000..67e83df --- /dev/null +++ b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/SOURCE.md @@ -0,0 +1,7 @@ +# Breezy Weather Icon Pack + +- Source: https://github.com/breezy-weather/breezy-weather +- Related icon-pack documentation: https://github.com/breezy-weather/breezy-weather-icon-packs +- Imported path: `app/src/main/res/drawable/weather_*.png` +- License note: Breezy Weather code is LGPL-3.0; verify individual asset terms before external redistribution. +- Usage: bundled as a selectable weather visual style at user request. diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day.png new file mode 100644 index 0000000..d1551fa Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_1.png new file mode 100644 index 0000000..1c01f83 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_2.png new file mode 100644 index 0000000..a129f51 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_dark.png new file mode 100644 index 0000000..f972b50 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_grey.png new file mode 100644 index 0000000..7b44e01 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_light.png new file mode 100644 index 0000000..70ca21c Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_day_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night.png new file mode 100644 index 0000000..6091510 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_dark.png new file mode 100644 index 0000000..089563a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_grey.png new file mode 100644 index 0000000..6f9aa69 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_light.png new file mode 100644 index 0000000..39bd0d7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_clear_night_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy.png new file mode 100644 index 0000000..516ddd2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_1.png new file mode 100644 index 0000000..59c2286 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_2.png new file mode 100644 index 0000000..ed20c9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_dark.png new file mode 100644 index 0000000..57f5dd7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_grey.png new file mode 100644 index 0000000..d942e77 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_light.png new file mode 100644 index 0000000..4871637 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_cloudy_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog.png new file mode 100644 index 0000000..9482fc7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_dark.png new file mode 100644 index 0000000..5827d6c Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_grey.png new file mode 100644 index 0000000..fbaf01d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_light.png new file mode 100644 index 0000000..9c072e3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_fog_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail.png new file mode 100644 index 0000000..5380072 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_1.png new file mode 100644 index 0000000..ed20c9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_2.png new file mode 100644 index 0000000..b52fa8c Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_3.png new file mode 100644 index 0000000..544e457 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_dark.png new file mode 100644 index 0000000..d49ebc3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_grey.png new file mode 100644 index 0000000..861a46e Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_light.png new file mode 100644 index 0000000..4d785cd Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_hail_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze.png new file mode 100644 index 0000000..7a9848a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_1.png new file mode 100644 index 0000000..46a208b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_2.png new file mode 100644 index 0000000..2c7e671 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_3.png new file mode 100644 index 0000000..d71873b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_dark.png new file mode 100644 index 0000000..6a8e1f2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_grey.png new file mode 100644 index 0000000..df168a1 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_light.png new file mode 100644 index 0000000..66cd62d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_haze_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day.png new file mode 100644 index 0000000..fb3ecf9 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_1.png new file mode 100644 index 0000000..517f2c5 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_2.png new file mode 100644 index 0000000..1c01f83 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_3.png new file mode 100644 index 0000000..a129f51 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_dark.png new file mode 100644 index 0000000..df13207 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_grey.png new file mode 100644 index 0000000..5764fc5 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_light.png new file mode 100644 index 0000000..67a3d43 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_day_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night.png new file mode 100644 index 0000000..9a58b33 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_1.png new file mode 100644 index 0000000..c1162f1 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_2.png new file mode 100644 index 0000000..6091510 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_dark.png new file mode 100644 index 0000000..2e628a6 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_grey.png new file mode 100644 index 0000000..76e9f25 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_light.png new file mode 100644 index 0000000..f729b82 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_partly_cloudy_night_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain.png new file mode 100644 index 0000000..fdf4225 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_1.png new file mode 100644 index 0000000..ed20c9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_2.png new file mode 100644 index 0000000..b200253 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_3.png new file mode 100644 index 0000000..d49a0f8 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_dark.png new file mode 100644 index 0000000..d88817f Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_grey.png new file mode 100644 index 0000000..0671090 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_light.png new file mode 100644 index 0000000..e9fadd2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_rain_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet.png new file mode 100644 index 0000000..e74eba8 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_1.png new file mode 100644 index 0000000..ed20c9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_2.png new file mode 100644 index 0000000..cccae4d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_3.png new file mode 100644 index 0000000..d49a0f8 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_dark.png new file mode 100644 index 0000000..e1c4e86 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_grey.png new file mode 100644 index 0000000..fd85526 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_light.png new file mode 100644 index 0000000..91b5bfd Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_sleet_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow.png new file mode 100644 index 0000000..3bd0483 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_1.png new file mode 100644 index 0000000..ed20c9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_2.png new file mode 100644 index 0000000..cccae4d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_3.png new file mode 100644 index 0000000..5d9581d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_dark.png new file mode 100644 index 0000000..1e5fb0b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_grey.png new file mode 100644 index 0000000..b62ce30 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_light.png new file mode 100644 index 0000000..86d7f8c Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_snow_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder.png new file mode 100644 index 0000000..bbf8229 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_1.png new file mode 100644 index 0000000..ed20c9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_2.png new file mode 100644 index 0000000..a57bd6b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_dark.png new file mode 100644 index 0000000..7d2338a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_grey.png new file mode 100644 index 0000000..c27bc94 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_light.png new file mode 100644 index 0000000..4935cee Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunder_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm.png new file mode 100644 index 0000000..b9dce6a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_1.png new file mode 100644 index 0000000..ed20c9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_2.png new file mode 100644 index 0000000..a57bd6b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_3.png new file mode 100644 index 0000000..d49a0f8 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_dark.png new file mode 100644 index 0000000..cdf457e Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_grey.png new file mode 100644 index 0000000..a7e2943 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_light.png new file mode 100644 index 0000000..8786175 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_thunderstorm_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind.png new file mode 100644 index 0000000..ec99bca Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_dark.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_dark.png new file mode 100644 index 0000000..f84b8b8 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_dark.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_grey.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_grey.png new file mode 100644 index 0000000..3e75836 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_grey.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_light.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_light.png new file mode 100644 index 0000000..4e32c03 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/breezy/weather_wind_mini_light.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/SOURCE.md b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/SOURCE.md new file mode 100644 index 0000000..a555e99 --- /dev/null +++ b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/SOURCE.md @@ -0,0 +1,7 @@ +# Geometric Weather Icon Pack + +- Source: https://github.com/breezy-weather/geometric-icon-provider +- Related references: https://github.com/WangDaYeeeeee/GeometricWeather and https://app.sharess.cn/page/app/detail?id=hfkx5khY8cWw6bTLWKadOg%3D%3D +- Imported path: `app/src/main/res/drawable/weather_*.png` +- License note: provider code is LGPL-3.0; related Breezy icon-pack documentation marks Geometric assets as non-free. +- Usage: bundled as a selectable weather visual style at user request. diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_clear_day_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_clear_day_geometric.png new file mode 100644 index 0000000..66c2ca3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_clear_day_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_clear_night_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_clear_night_geometric.png new file mode 100644 index 0000000..8b0e04e Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_clear_night_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_cloudy_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_cloudy_geometric.png new file mode 100644 index 0000000..16455ad Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_cloudy_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_fog_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_fog_geometric.png new file mode 100644 index 0000000..e1561ea Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_fog_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_hail_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_hail_geometric.png new file mode 100644 index 0000000..a8bade0 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_hail_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_haze_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_haze_geometric.png new file mode 100644 index 0000000..f2a9468 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_haze_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_partly_cloudy_day_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_partly_cloudy_day_geometric.png new file mode 100644 index 0000000..e7f1d57 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_partly_cloudy_day_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_partly_cloudy_night_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_partly_cloudy_night_geometric.png new file mode 100644 index 0000000..c71be3c Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_partly_cloudy_night_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_rain_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_rain_geometric.png new file mode 100644 index 0000000..4937768 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_rain_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_sleet_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_sleet_geometric.png new file mode 100644 index 0000000..f4ae772 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_sleet_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_snow_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_snow_geometric.png new file mode 100644 index 0000000..7f65312 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_snow_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_thunder_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_thunder_geometric.png new file mode 100644 index 0000000..d002bec Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_thunder_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_wind_geometric.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_wind_geometric.png new file mode 100644 index 0000000..c418bb9 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/geometric/weather_wind_geometric.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/SOURCE.md b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/SOURCE.md new file mode 100644 index 0000000..478aede --- /dev/null +++ b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/SOURCE.md @@ -0,0 +1,6 @@ +# Google Weather v4 Icon Pack + +- Source: https://github.com/mbatthew/GoogleWeatherIconsV4Pack +- Imported path: `app/src/main/res/drawable/weather_*.png` +- License note: repository code is MIT; README states the included weather icons are sourced from Google Weather Icons v4 and have uncertain licensing. +- Usage: bundled as a selectable weather visual style at user request. diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_clear_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_clear_day.png new file mode 100644 index 0000000..236b803 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_clear_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_clear_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_clear_night.png new file mode 100644 index 0000000..bc66a1e Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_clear_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_day.png new file mode 100644 index 0000000..59c7a84 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night.png new file mode 100644 index 0000000..17ede06 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night_1.png new file mode 100644 index 0000000..59c7a84 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night_2.png new file mode 100644 index 0000000..373ec0d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_cloudy_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day.png new file mode 100644 index 0000000..5fbc9f7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day_1.png new file mode 100644 index 0000000..1a85e95 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day_2.png new file mode 100644 index 0000000..d994236 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night.png new file mode 100644 index 0000000..fdf1ee3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_1.png new file mode 100644 index 0000000..081d7f3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_2.png new file mode 100644 index 0000000..f8688d4 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_3.png new file mode 100644 index 0000000..61e5fa2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_fog_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day.png new file mode 100644 index 0000000..42683cf Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_1.png new file mode 100644 index 0000000..6052ca3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_2.png new file mode 100644 index 0000000..46d6f2c Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_3.png new file mode 100644 index 0000000..a2799b7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night.png new file mode 100644 index 0000000..35f365d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_1.png new file mode 100644 index 0000000..7980012 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_2.png new file mode 100644 index 0000000..a2799b7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_3.png new file mode 100644 index 0000000..e17f068 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_hail_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day.png new file mode 100644 index 0000000..1bd7efe Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day_1.png new file mode 100644 index 0000000..bee0dc6 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day_2.png new file mode 100644 index 0000000..9aae6b7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night.png new file mode 100644 index 0000000..36345a4 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_1.png new file mode 100644 index 0000000..b918939 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_2.png new file mode 100644 index 0000000..a714fed Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_3.png new file mode 100644 index 0000000..61e5fa2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_haze_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day.png new file mode 100644 index 0000000..52985fe Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day_1.png new file mode 100644 index 0000000..663db49 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day_2.png new file mode 100644 index 0000000..399231b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night.png new file mode 100644 index 0000000..8b8d0f7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night_1.png new file mode 100644 index 0000000..27d1073 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night_2.png new file mode 100644 index 0000000..d55197d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_partly_cloudy_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day.png new file mode 100644 index 0000000..5b3ac0f Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_1.png new file mode 100644 index 0000000..2e369fd Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_2.png new file mode 100644 index 0000000..62532f8 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_3.png new file mode 100644 index 0000000..db0554a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night.png new file mode 100644 index 0000000..c9972ef Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_1.png new file mode 100644 index 0000000..60dc896 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_2.png new file mode 100644 index 0000000..a8eb9aa Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_3.png new file mode 100644 index 0000000..a96de3f Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_rain_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day.png new file mode 100644 index 0000000..0203cb7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_1.png new file mode 100644 index 0000000..3f3e3f7 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_2.png new file mode 100644 index 0000000..fca96f9 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_3.png new file mode 100644 index 0000000..192ae8d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night.png new file mode 100644 index 0000000..09897b3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_1.png new file mode 100644 index 0000000..0396917 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_2.png new file mode 100644 index 0000000..192ae8d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_3.png new file mode 100644 index 0000000..97ff064 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_sleet_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day.png new file mode 100644 index 0000000..7e0447c Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_1.png new file mode 100644 index 0000000..77352e1 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_2.png new file mode 100644 index 0000000..fdbb062 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_3.png new file mode 100644 index 0000000..db0554a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night.png new file mode 100644 index 0000000..7418d9b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_1.png new file mode 100644 index 0000000..f74fdfc Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_2.png new file mode 100644 index 0000000..6f4f91a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_3.png new file mode 100644 index 0000000..61e5fa2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_snow_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day.png new file mode 100644 index 0000000..641e5c2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_1.png new file mode 100644 index 0000000..3ef4d27 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_2.png new file mode 100644 index 0000000..5f7ea00 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_3.png new file mode 100644 index 0000000..b44f186 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night.png new file mode 100644 index 0000000..c44d2a1 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_1.png new file mode 100644 index 0000000..2667639 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_2.png new file mode 100644 index 0000000..b44f186 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_3.png new file mode 100644 index 0000000..deab983 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunder_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day.png new file mode 100644 index 0000000..52ae7be Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_1.png new file mode 100644 index 0000000..bc6cc50 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_2.png new file mode 100644 index 0000000..1b5c9b0 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_3.png new file mode 100644 index 0000000..257051d Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night.png new file mode 100644 index 0000000..9526dcc Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_1.png new file mode 100644 index 0000000..b3e6154 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_2.png new file mode 100644 index 0000000..73fe2dd Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_3.png new file mode 100644 index 0000000..036e4f0 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_thunderstorm_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day.png new file mode 100644 index 0000000..09eabce Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_1.png new file mode 100644 index 0000000..4f9d0a1 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_2.png new file mode 100644 index 0000000..f5f492a Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_3.png new file mode 100644 index 0000000..cf1ff97 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_day_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night.png new file mode 100644 index 0000000..1a084d8 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_1.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_1.png new file mode 100644 index 0000000..a01e8f3 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_1.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_2.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_2.png new file mode 100644 index 0000000..9873a52 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_2.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_3.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_3.png new file mode 100644 index 0000000..61e5fa2 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/google-weather-v4/weather_wind_night_3.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/SOURCE.md b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/SOURCE.md new file mode 100644 index 0000000..99e5422 --- /dev/null +++ b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/SOURCE.md @@ -0,0 +1,6 @@ +# Lemon Flutter Weather Icon Pack + +- Source: https://github.com/yangSpica27/spica_weather_flutter +- Imported path: `assets/ic_*.png` +- License note: repository is MIT licensed. +- Usage: bundled as a selectable weather visual style at user request. diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_app.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_app.png new file mode 100644 index 0000000..4b0415e Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_app.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_bike.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_bike.png new file mode 100644 index 0000000..8609a71 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_bike.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_car.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_car.png new file mode 100644 index 0000000..387cd5b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_car.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_cloud.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_cloud.png new file mode 100644 index 0000000..5945652 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_cloud.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_cloudy.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_cloudy.png new file mode 100644 index 0000000..e13bebb Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_cloudy.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_coat_hanger.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_coat_hanger.png new file mode 100644 index 0000000..ea51d8b Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_coat_hanger.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_dashboard.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_dashboard.png new file mode 100644 index 0000000..94db451 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_dashboard.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_light_rain.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_light_rain.png new file mode 100644 index 0000000..d511fba Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_light_rain.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_rain.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_rain.png new file mode 100644 index 0000000..40e2248 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_rain.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_snow.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_snow.png new file mode 100644 index 0000000..6c6b60f Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_snow.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_storm.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_storm.png new file mode 100644 index 0000000..48cf936 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_storm.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_sun.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_sun.png new file mode 100644 index 0000000..398ddc9 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_sun.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_sun_hat.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_sun_hat.png new file mode 100644 index 0000000..f327034 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_sun_hat.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_telescope.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_telescope.png new file mode 100644 index 0000000..21994e9 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_telescope.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_thunder.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_thunder.png new file mode 100644 index 0000000..df5ed58 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_thunder.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_volleyball.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_volleyball.png new file mode 100644 index 0000000..b919439 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_volleyball.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_water.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_water.png new file mode 100644 index 0000000..723e591 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_water.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_wave.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_wave.png new file mode 100644 index 0000000..84d3f36 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_wave.png differ diff --git a/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_windmill.png b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_windmill.png new file mode 100644 index 0000000..aa53e30 Binary files /dev/null and b/LanMountainDesktop/Assets/MaterialWeatherIcons/lemon-flutter/ic_windmill.png differ diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index db0b84b..5121bb6 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -1436,5 +1436,25 @@ "power.sleep_confirm_title": "Sleep Confirmation", "power.sleep_confirm_message": "Are you sure you want to put the computer to sleep?", "power.confirm_yes": "Yes", - "power.confirm_cancel": "Cancel" - } + "power.confirm_cancel": "Cancel", + "settings.weather.visual_style_header": "Weather Visual Style", + "settings.weather.visual_style_desc": "Choose the icon and component style used by desktop weather widgets.", + "settings.weather.visual_style.GoogleWeatherV4": "Google Weather v4", + "settings.weather.visual_style.Geometric": "Geometric", + "settings.weather.visual_style.Breezy": "Breezy Weather", + "settings.weather.visual_style.LemonFlutter": "Lemon Weather Flutter", + "settings.general.back_to_windows_button_display_header": "Back to platform button", + "settings.general.back_to_windows_button_display_desc": "Choose whether the Dock button shows its circle icon, text, or both.", + "settings.general.back_to_windows_button_display.icon_and_text": "Icon and text", + "settings.general.back_to_windows_button_display.icon_only": "Icon only", + "settings.general.back_to_windows_button_display.text_only": "Text only", + "settings.general.back_to_windows_icon_source_header": "Back button icon source", + "settings.general.back_to_windows_icon_source_desc": "Choose whether the left icon slot uses a Fluent icon or short custom text.", + "settings.general.back_to_windows_icon_source.fluent_icon": "Fluent icon", + "settings.general.back_to_windows_icon_source.text": "Text icon", + "settings.general.back_to_windows_fluent_icon_header": "Fluent icon", + "settings.general.back_to_windows_fluent_icon_desc": "Search and choose a built-in Fluent icon for the left icon slot.", + "settings.general.back_to_windows_icon_text_header": "Text icon", + "settings.general.back_to_windows_icon_text_desc": "Enter up to four characters to display as the left icon.", + "settings.general.back_to_windows_fluent_icon_search_placeholder": "Search icon" +} diff --git a/LanMountainDesktop/Localization/ja-JP.json b/LanMountainDesktop/Localization/ja-JP.json index 6bce69e..5dbeb3d 100644 --- a/LanMountainDesktop/Localization/ja-JP.json +++ b/LanMountainDesktop/Localization/ja-JP.json @@ -1118,5 +1118,25 @@ "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": "カラースキーム" + "component.settings.color_scheme": "カラースキーム", + "settings.weather.visual_style_header": "天気ビジュアルスタイル", + "settings.weather.visual_style_desc": "デスクトップ天気ウィジェットのアイコンとスタイルを選択します。", + "settings.weather.visual_style.GoogleWeatherV4": "Google Weather v4", + "settings.weather.visual_style.Geometric": "Geometric", + "settings.weather.visual_style.Breezy": "Breezy Weather", + "settings.weather.visual_style.LemonFlutter": "Lemon Weather Flutter", + "settings.general.back_to_windows_button_display_header": "プラットフォームに戻るボタン", + "settings.general.back_to_windows_button_display_desc": "Dock ボタンに円形アイコン、テキスト、またはその両方を表示するかを選択します。", + "settings.general.back_to_windows_button_display.icon_and_text": "アイコンとテキスト", + "settings.general.back_to_windows_button_display.icon_only": "アイコンのみ", + "settings.general.back_to_windows_button_display.text_only": "テキストのみ", + "settings.general.back_to_windows_icon_source_header": "戻るボタンのアイコンソース", + "settings.general.back_to_windows_icon_source_desc": "左側のアイコン枠に Fluent アイコンまたは短いテキストを使うかを選択します。", + "settings.general.back_to_windows_icon_source.fluent_icon": "Fluent アイコン", + "settings.general.back_to_windows_icon_source.text": "テキストアイコン", + "settings.general.back_to_windows_fluent_icon_header": "Fluent アイコン", + "settings.general.back_to_windows_fluent_icon_desc": "左側のアイコン枠に使う組み込み Fluent アイコンを検索して選択します。", + "settings.general.back_to_windows_icon_text_header": "テキストアイコン", + "settings.general.back_to_windows_icon_text_desc": "左側のアイコンとして表示する文字を最大4文字まで入力します。", + "settings.general.back_to_windows_fluent_icon_search_placeholder": "アイコンを検索" } diff --git a/LanMountainDesktop/Localization/ko-KR.json b/LanMountainDesktop/Localization/ko-KR.json index 41cf02b..2ae6c2d 100644 --- a/LanMountainDesktop/Localization/ko-KR.json +++ b/LanMountainDesktop/Localization/ko-KR.json @@ -1164,5 +1164,25 @@ "single_instance.notice.description": "앱이 이미 실행 중이므로 여러 번 클릭하여 열 필요가 없습니다.", "single_instance.notice.button": "확인", "market.status.install_success_restart_format": "✓ 플러그인 '{0}' 설치 성공! 활성화하려면 앱을 재시작하세요.", - "market.dialog.restart_message_format": "플러그인 '{0}'이(가) 성공적으로 설치되었습니다.\n\n이 플러그인을 사용하려면 앱을 즉시 재시작해야 합니다.\n\n지금 재시작하시겠습니까?" - } + "market.dialog.restart_message_format": "플러그인 '{0}'이(가) 성공적으로 설치되었습니다.\n\n이 플러그인을 사용하려면 앱을 즉시 재시작해야 합니다.\n\n지금 재시작하시겠습니까?", + "settings.weather.visual_style_header": "날씨 비주얼 스타일", + "settings.weather.visual_style_desc": "데스크톱 날씨 위젯에 사용할 아이콘과 스타일을 선택합니다.", + "settings.weather.visual_style.GoogleWeatherV4": "Google Weather v4", + "settings.weather.visual_style.Geometric": "Geometric", + "settings.weather.visual_style.Breezy": "Breezy Weather", + "settings.weather.visual_style.LemonFlutter": "Lemon Weather Flutter", + "settings.general.back_to_windows_button_display_header": "플랫폼으로 돌아가기 버튼", + "settings.general.back_to_windows_button_display_desc": "Dock 버튼에 원형 아이콘, 텍스트 또는 둘 다 표시할지 선택합니다.", + "settings.general.back_to_windows_button_display.icon_and_text": "아이콘과 텍스트", + "settings.general.back_to_windows_button_display.icon_only": "아이콘만", + "settings.general.back_to_windows_button_display.text_only": "텍스트만", + "settings.general.back_to_windows_icon_source_header": "돌아가기 버튼 아이콘 소스", + "settings.general.back_to_windows_icon_source_desc": "왼쪽 아이콘 영역에 Fluent 아이콘 또는 짧은 텍스트를 사용할지 선택합니다.", + "settings.general.back_to_windows_icon_source.fluent_icon": "Fluent 아이콘", + "settings.general.back_to_windows_icon_source.text": "텍스트 아이콘", + "settings.general.back_to_windows_fluent_icon_header": "Fluent 아이콘", + "settings.general.back_to_windows_fluent_icon_desc": "왼쪽 아이콘 영역에 사용할 내장 Fluent 아이콘을 검색하고 선택합니다.", + "settings.general.back_to_windows_icon_text_header": "텍스트 아이콘", + "settings.general.back_to_windows_icon_text_desc": "왼쪽 아이콘으로 표시할 문자를 최대 4자까지 입력합니다.", + "settings.general.back_to_windows_fluent_icon_search_placeholder": "아이콘 검색" +} diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index 29fb92a..9a5ebdf 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -1367,5 +1367,25 @@ "power.sleep_confirm_title": "睡眠确认", "power.sleep_confirm_message": "确定要让计算机进入睡眠状态吗?", "power.confirm_yes": "确定", - "power.confirm_cancel": "取消" + "power.confirm_cancel": "取消", + "settings.weather.visual_style_header": "天气视觉风格", + "settings.weather.visual_style_desc": "选择桌面天气组件使用的图标与组件风格。", + "settings.weather.visual_style.GoogleWeatherV4": "Google 天气 v4", + "settings.weather.visual_style.Geometric": "几何天气", + "settings.weather.visual_style.Breezy": "Breezy Weather", + "settings.weather.visual_style.LemonFlutter": "柠檬天气 Flutter", + "settings.general.back_to_windows_button_display_header": "回到系统按钮", + "settings.general.back_to_windows_button_display_desc": "选择 Dock 按钮显示圆形图标、文字,或同时显示两者。", + "settings.general.back_to_windows_button_display.icon_and_text": "图标和文字", + "settings.general.back_to_windows_button_display.icon_only": "仅图标", + "settings.general.back_to_windows_button_display.text_only": "仅文字", + "settings.general.back_to_windows_icon_source_header": "返回按钮图标来源", + "settings.general.back_to_windows_icon_source_desc": "选择左侧图标位使用 Fluent 图标,或使用短文字。", + "settings.general.back_to_windows_icon_source.fluent_icon": "Fluent 图标", + "settings.general.back_to_windows_icon_source.text": "文字图标", + "settings.general.back_to_windows_fluent_icon_header": "Fluent 图标", + "settings.general.back_to_windows_fluent_icon_desc": "搜索并选择左侧图标位使用的内置 Fluent 图标。", + "settings.general.back_to_windows_icon_text_header": "文字图标", + "settings.general.back_to_windows_icon_text_desc": "输入最多四个字符,作为左侧图标显示。", + "settings.general.back_to_windows_fluent_icon_search_placeholder": "搜索图标" } diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index a081a2e..9863343 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -69,7 +69,7 @@ public sealed class AppSettingsSnapshot public string WeatherExcludedAlerts { get; set; } = string.Empty; - public string WeatherIconPackId { get; set; } = "DefaultWeather"; + public string WeatherIconPackId { get; set; } = "GoogleWeatherV4"; public bool WeatherNoTlsRequests { get; set; } @@ -120,6 +120,14 @@ public sealed class AppSettingsSnapshot public string TaskbarLayoutMode { get; set; } = "BottomFullRowMacStyle"; + public string BackToWindowsButtonDisplayMode { get; set; } = "IconAndText"; + + public string BackToWindowsIconSource { get; set; } = "FluentIcon"; + + public string BackToWindowsFluentIconName { get; set; } = "Circle"; + + public string BackToWindowsIconText { get; set; } = "○"; + public string ClockDisplayFormat { get; set; } = "HourMinuteSecond"; public bool StatusBarClockTransparentBackground { get; set; } diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index 84adf2b..3136585 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -668,8 +668,7 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposa private static string NormalizeIconPackId(string? iconPackId) { - _ = iconPackId; - return "DefaultWeather"; + return WeatherVisualStyleCatalog.Normalize(iconPackId); } } diff --git a/LanMountainDesktop/Services/WeatherIconAssetResolver.cs b/LanMountainDesktop/Services/WeatherIconAssetResolver.cs new file mode 100644 index 0000000..f8e986b --- /dev/null +++ b/LanMountainDesktop/Services/WeatherIconAssetResolver.cs @@ -0,0 +1,235 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using LanMountainDesktop.Models; + +namespace LanMountainDesktop.Services; + +public static class WeatherIconAssetResolver +{ + private const string RootUri = "avares://LanMountainDesktop/Assets/MaterialWeatherIcons"; + + public static Bitmap? LoadIcon(string? styleId, WeatherSnapshot? snapshot) + { + return LoadIcon(styleId, ResolveIconKey(snapshot)); + } + + public static Bitmap? LoadIcon(string? styleId, int? weatherCode, string? weatherText, bool isDaylight = true) + { + return LoadIcon(styleId, ResolveIconKey(weatherCode, weatherText, isDaylight)); + } + + public static Bitmap? LoadIcon(string? styleId, string iconKey) + { + var uri = ResolveAssetUri(styleId, iconKey); + if (uri is null) + { + return null; + } + + using var stream = AssetLoader.Open(uri); + return new Bitmap(stream); + } + + public static Uri? ResolveAssetUri(string? styleId, WeatherSnapshot? snapshot) + { + return ResolveAssetUri(styleId, ResolveIconKey(snapshot)); + } + + public static Uri? ResolveAssetUri(string? styleId, int? weatherCode, string? weatherText, bool isDaylight = true) + { + return ResolveAssetUri(styleId, ResolveIconKey(weatherCode, weatherText, isDaylight)); + } + + public static Uri? ResolveAssetUri(string? styleId, string iconKey) + { + var style = WeatherVisualStyleCatalog.GetStyle(styleId); + return TryBuildUri(style, iconKey) + ?? TryBuildUri(style, NormalizeDayNightFallback(iconKey)) + ?? TryBuildUri(style, "cloudy_day") + ?? TryBuildUri(WeatherVisualStyleCatalog.GetDefault(), iconKey) + ?? TryBuildUri(WeatherVisualStyleCatalog.GetDefault(), NormalizeDayNightFallback(iconKey)) + ?? TryBuildUri(WeatherVisualStyleCatalog.GetDefault(), "cloudy_day"); + } + + public static string ResolveIconKey(WeatherSnapshot? snapshot) + { + var current = snapshot?.Current; + var isDaylight = current?.IsDaylight ?? true; + return ResolveIconKey(current?.WeatherCode, current?.WeatherText, isDaylight); + } + + public static string ResolveIconKey(int? weatherCode, string? weatherText, bool isDaylight) + { + var dayNight = isDaylight ? "day" : "night"; + var condition = ResolveCondition(weatherCode, weatherText); + return condition switch + { + "clear" => $"clear_{dayNight}", + "partly_cloudy" => $"partly_cloudy_{dayNight}", + "cloudy" => $"cloudy_{dayNight}", + "rain" => $"rain_{dayNight}", + "sleet" => $"sleet_{dayNight}", + "snow" => $"snow_{dayNight}", + "hail" => $"hail_{dayNight}", + "thunder" => $"thunder_{dayNight}", + "thunderstorm" => $"thunderstorm_{dayNight}", + "fog" => $"fog_{dayNight}", + "haze" => $"haze_{dayNight}", + "wind" => $"wind_{dayNight}", + _ => $"cloudy_{dayNight}" + }; + } + + private static Uri? TryBuildUri(WeatherVisualStyleDefinition style, string iconKey) + { + var fileName = ResolveFileName(style.Id, iconKey); + if (string.IsNullOrWhiteSpace(fileName)) + { + return null; + } + + var uri = new Uri($"{RootUri}/{style.AssetFolder}/{fileName}", UriKind.Absolute); + try + { + return AssetLoader.Exists(uri) ? uri : null; + } + catch (InvalidOperationException) + { + return uri; + } + } + + private static string ResolveFileName(string styleId, string iconKey) + { + var normalized = NormalizeDayNightFallback(iconKey); + return styleId switch + { + WeatherVisualStyleId.GoogleWeatherV4 => ResolveGoogleFileName(iconKey), + WeatherVisualStyleId.Geometric => ResolveGeometricFileName(normalized), + WeatherVisualStyleId.Breezy => ResolveBreezyFileName(normalized), + WeatherVisualStyleId.LemonFlutter => ResolveLemonFileName(normalized), + _ => ResolveGoogleFileName(iconKey) + }; + } + + private static string ResolveGoogleFileName(string iconKey) + { + return iconKey switch + { + "cloudy_day" => "weather_cloudy_day.png", + "cloudy_night" => "weather_cloudy_night.png", + _ => $"weather_{iconKey}.png" + }; + } + + private static string ResolveGeometricFileName(string iconKey) + { + return iconKey switch + { + "clear_day" or "clear_night" or "partly_cloudy_day" or "partly_cloudy_night" => $"weather_{iconKey}_geometric.png", + "cloudy_day" or "cloudy_night" => "weather_cloudy_geometric.png", + "rain_day" or "rain_night" => "weather_rain_geometric.png", + "sleet_day" or "sleet_night" => "weather_sleet_geometric.png", + "snow_day" or "snow_night" => "weather_snow_geometric.png", + "hail_day" or "hail_night" => "weather_hail_geometric.png", + "fog_day" or "fog_night" => "weather_fog_geometric.png", + "haze_day" or "haze_night" => "weather_haze_geometric.png", + "wind_day" or "wind_night" => "weather_wind_geometric.png", + "thunderstorm_day" or "thunderstorm_night" => "weather_thunder_geometric.png", + "thunder_day" or "thunder_night" => "weather_thunder_geometric.png", + _ => $"weather_{iconKey}_geometric.png" + }; + } + + private static string ResolveBreezyFileName(string iconKey) + { + return iconKey switch + { + "clear_day" or "clear_night" or "partly_cloudy_day" or "partly_cloudy_night" => $"weather_{iconKey}.png", + "cloudy_day" or "cloudy_night" => "weather_cloudy.png", + "rain_day" or "rain_night" => "weather_rain.png", + "sleet_day" or "sleet_night" => "weather_sleet.png", + "snow_day" or "snow_night" => "weather_snow.png", + "hail_day" or "hail_night" => "weather_hail.png", + "fog_day" or "fog_night" => "weather_fog.png", + "haze_day" or "haze_night" => "weather_haze.png", + "wind_day" or "wind_night" => "weather_wind.png", + "thunder_day" or "thunder_night" => "weather_thunder.png", + "thunderstorm_day" or "thunderstorm_night" => "weather_thunderstorm.png", + _ => $"weather_{iconKey}.png" + }; + } + + private static string ResolveLemonFileName(string iconKey) + { + return iconKey switch + { + "clear_day" or "clear_night" => "ic_sun.png", + "partly_cloudy_day" or "partly_cloudy_night" => "ic_cloudy.png", + "cloudy_day" or "cloudy_night" => "ic_cloud.png", + "rain_day" or "rain_night" => "ic_rain.png", + "sleet_day" or "sleet_night" => "ic_light_rain.png", + "snow_day" or "snow_night" => "ic_snow.png", + "hail_day" or "hail_night" => "ic_storm.png", + "thunder_day" or "thunder_night" => "ic_thunder.png", + "thunderstorm_day" or "thunderstorm_night" => "ic_storm.png", + "fog_day" or "fog_night" => "ic_cloudy.png", + "haze_day" or "haze_night" => "ic_cloudy.png", + "wind_day" or "wind_night" => "ic_windmill.png", + _ => "ic_cloud.png" + }; + } + + private static string NormalizeDayNightFallback(string iconKey) + { + return iconKey switch + { + "cloudy_night" => "cloudy_day", + "rain_night" => "rain_day", + "sleet_night" => "sleet_day", + "snow_night" => "snow_day", + "hail_night" => "hail_day", + "thunder_night" => "thunder_day", + "thunderstorm_night" => "thunderstorm_day", + "fog_night" => "fog_day", + "haze_night" => "haze_day", + "wind_night" => "wind_day", + _ => iconKey + }; + } + + private static string ResolveCondition(int? weatherCode, string? weatherText) + { + if (weatherCode.HasValue) + { + return weatherCode.Value switch + { + 0 => "clear", + 1 => "partly_cloudy", + 2 => "cloudy", + 3 or 7 or 8 or 9 or 10 or 11 or 12 or 19 or 21 or 22 or 23 or 24 or 25 or 301 => "rain", + 4 or 5 => "thunderstorm", + 6 or 13 or 14 or 15 or 16 or 17 or 26 or 27 or 28 or 302 => "snow", + 18 or 32 or 49 or 57 or 58 => "fog", + 20 or 29 or 30 or 31 or 53 or 54 or 55 or 56 => "haze", + _ => "cloudy" + }; + } + + var text = weatherText?.Trim().ToLowerInvariant() ?? string.Empty; + if (text.Contains("thunderstorm") || text.Contains('\u96f7')) return "thunderstorm"; + if (text.Contains("thunder") || text.Contains("storm")) return "thunder"; + if (text.Contains("sleet")) return "sleet"; + if (text.Contains("hail")) return "hail"; + if (text.Contains("snow") || text.Contains('\u96ea')) return "snow"; + if (text.Contains("rain") || text.Contains('\u96e8')) return "rain"; + if (text.Contains("fog") || text.Contains("mist") || text.Contains('\u96fe')) return "fog"; + if (text.Contains("haze") || text.Contains("dust") || text.Contains('\u973e')) return "haze"; + if (text.Contains("wind")) return "wind"; + if (text.Contains("partly")) return "partly_cloudy"; + if (text.Contains("cloud") || text.Contains('\u4e91') || text.Contains('\u9634')) return "cloudy"; + if (text.Contains("clear") || text.Contains("sun") || text.Contains('\u6674')) return "clear"; + return "cloudy"; + } +} diff --git a/LanMountainDesktop/Services/WeatherLocationRefreshService.cs b/LanMountainDesktop/Services/WeatherLocationRefreshService.cs index 6af73bd..e289ead 100644 --- a/LanMountainDesktop/Services/WeatherLocationRefreshService.cs +++ b/LanMountainDesktop/Services/WeatherLocationRefreshService.cs @@ -125,8 +125,7 @@ public sealed class WeatherLocationRefreshService private static string NormalizeIconPackId(string? iconPackId) { - _ = iconPackId; - return "DefaultWeather"; + return WeatherVisualStyleCatalog.Normalize(iconPackId); } private string BuildCoordinateDisplayName(string? languageCode, double latitude, double longitude) diff --git a/LanMountainDesktop/Services/WeatherVisualStyleCatalog.cs b/LanMountainDesktop/Services/WeatherVisualStyleCatalog.cs new file mode 100644 index 0000000..c0d089d --- /dev/null +++ b/LanMountainDesktop/Services/WeatherVisualStyleCatalog.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LanMountainDesktop.Services; + +public static class WeatherVisualStyleId +{ + public const string GoogleWeatherV4 = "GoogleWeatherV4"; + public const string Geometric = "Geometric"; + public const string Breezy = "Breezy"; + public const string LemonFlutter = "LemonFlutter"; + + public const string Default = GoogleWeatherV4; +} + +public sealed record WeatherVisualStyleDefinition( + string Id, + string DisplayName, + string AssetFolder, + string SourceDescription); + +public static class WeatherVisualStyleCatalog +{ + private static readonly WeatherVisualStyleDefinition[] Styles = + [ + new( + WeatherVisualStyleId.GoogleWeatherV4, + "Google Weather v4", + "google-weather-v4", + "Google Weather Icons v4 pack for Breezy Weather; icon licensing is uncertain."), + new( + WeatherVisualStyleId.Geometric, + "Geometric", + "geometric", + "Geometric Weather icon provider, compatible with Breezy Weather."), + new( + WeatherVisualStyleId.Breezy, + "Breezy Weather", + "breezy", + "Breezy Weather bundled icon resources."), + new( + WeatherVisualStyleId.LemonFlutter, + "Lemon Weather Flutter", + "lemon-flutter", + "spica_weather_flutter assets, MIT licensed.") + ]; + + public static IReadOnlyList GetStyles() => Styles; + + public static WeatherVisualStyleDefinition GetDefault() => Styles[0]; + + public static WeatherVisualStyleDefinition GetStyle(string? id) + { + var normalized = Normalize(id); + return Styles.First(style => string.Equals(style.Id, normalized, StringComparison.OrdinalIgnoreCase)); + } + + public static string Normalize(string? id) + { + if (string.IsNullOrWhiteSpace(id)) + { + return WeatherVisualStyleId.Default; + } + + var candidate = id.Trim(); + if (string.Equals(candidate, "DefaultWeather", StringComparison.OrdinalIgnoreCase) || + string.Equals(candidate, "HyperOS3", StringComparison.OrdinalIgnoreCase)) + { + return WeatherVisualStyleId.Default; + } + + return Styles.Any(style => string.Equals(style.Id, candidate, StringComparison.OrdinalIgnoreCase)) + ? Styles.First(style => string.Equals(style.Id, candidate, StringComparison.OrdinalIgnoreCase)).Id + : WeatherVisualStyleId.Default; + } +} diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs index eeec2db..3130ccc 100644 --- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs +++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs @@ -11,6 +11,7 @@ using Avalonia.Media; using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FluentIcons.Common; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; @@ -180,6 +181,22 @@ public sealed class SelectionOption public string Label { get; } } +public sealed class FluentIconSelectionOption +{ + public FluentIconSelectionOption(string value, string label, Icon icon) + { + Value = value; + Label = label; + Icon = icon; + } + + public string Value { get; } + + public string Label { get; } + + public Icon Icon { get; } +} + public sealed class ThemeSeedCandidateOption { public ThemeSeedCandidateOption(string value, string label, Color color, bool isSelected) @@ -237,6 +254,10 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo Languages = CreateLanguageOptions(); RenderModes = CreateRenderModeOptions(); MultiInstanceLaunchBehaviors = CreateMultiInstanceLaunchBehaviorOptions(); + BackToWindowsButtonDisplayModes = CreateBackToWindowsButtonDisplayModeOptions(); + BackToWindowsIconSources = CreateBackToWindowsIconSourceOptions(); + BackToWindowsFluentIcons = CreateBackToWindowsFluentIconOptions(); + FilteredBackToWindowsFluentIcons = BackToWindowsFluentIcons; TimeZones = CreateTimeZoneOptions(); RefreshLocalizedText(); @@ -257,6 +278,16 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo string.Equals(option.Value, appSnapshot.MultiInstanceLaunchBehavior.ToString(), StringComparison.OrdinalIgnoreCase)) ?? MultiInstanceLaunchBehaviors.First(option => string.Equals(option.Value, MultiInstanceLaunchBehavior.NotifyAndOpenDesktop.ToString(), StringComparison.OrdinalIgnoreCase)); + SelectedBackToWindowsButtonDisplayMode = BackToWindowsButtonDisplayModes.FirstOrDefault(option => + string.Equals(option.Value, NormalizeBackToWindowsButtonDisplayMode(appSnapshot.BackToWindowsButtonDisplayMode), StringComparison.OrdinalIgnoreCase)) + ?? BackToWindowsButtonDisplayModes[0]; + SelectedBackToWindowsIconSource = BackToWindowsIconSources.FirstOrDefault(option => + string.Equals(option.Value, NormalizeBackToWindowsIconSource(appSnapshot.BackToWindowsIconSource), StringComparison.OrdinalIgnoreCase)) + ?? BackToWindowsIconSources[0]; + SelectedBackToWindowsFluentIcon = BackToWindowsFluentIcons.FirstOrDefault(option => + string.Equals(option.Value, NormalizeBackToWindowsFluentIconName(appSnapshot.BackToWindowsFluentIconName), StringComparison.OrdinalIgnoreCase)) + ?? BackToWindowsFluentIcons.First(option => string.Equals(option.Value, "Circle", StringComparison.OrdinalIgnoreCase)); + BackToWindowsIconText = NormalizeBackToWindowsIconText(appSnapshot.BackToWindowsIconText); ApplyTransitionPreferences(appSnapshot.EnableFadeTransition, appSnapshot.EnableSlideTransition); ShowInTaskbar = appSnapshot.ShowInTaskbar; _isInitializing = false; @@ -311,6 +342,28 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo ?? MultiInstanceLaunchBehaviors.First(option => string.Equals(option.Value, MultiInstanceLaunchBehavior.NotifyAndOpenDesktop.ToString(), StringComparison.OrdinalIgnoreCase)); } + + if (changedKeys.Contains(nameof(AppSettingsSnapshot.BackToWindowsButtonDisplayMode))) + { + var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App); + SelectedBackToWindowsButtonDisplayMode = BackToWindowsButtonDisplayModes.FirstOrDefault(option => + string.Equals(option.Value, NormalizeBackToWindowsButtonDisplayMode(snapshot.BackToWindowsButtonDisplayMode), StringComparison.OrdinalIgnoreCase)) + ?? BackToWindowsButtonDisplayModes[0]; + } + + if (changedKeys.Contains(nameof(AppSettingsSnapshot.BackToWindowsIconSource)) || + changedKeys.Contains(nameof(AppSettingsSnapshot.BackToWindowsFluentIconName)) || + changedKeys.Contains(nameof(AppSettingsSnapshot.BackToWindowsIconText))) + { + var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App); + SelectedBackToWindowsIconSource = BackToWindowsIconSources.FirstOrDefault(option => + string.Equals(option.Value, NormalizeBackToWindowsIconSource(snapshot.BackToWindowsIconSource), StringComparison.OrdinalIgnoreCase)) + ?? BackToWindowsIconSources[0]; + SelectedBackToWindowsFluentIcon = BackToWindowsFluentIcons.FirstOrDefault(option => + string.Equals(option.Value, NormalizeBackToWindowsFluentIconName(snapshot.BackToWindowsFluentIconName), StringComparison.OrdinalIgnoreCase)) + ?? BackToWindowsFluentIcons.First(option => string.Equals(option.Value, "Circle", StringComparison.OrdinalIgnoreCase)); + BackToWindowsIconText = NormalizeBackToWindowsIconText(snapshot.BackToWindowsIconText); + } } public event Action? RestartRequested; @@ -321,6 +374,12 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo public IReadOnlyList MultiInstanceLaunchBehaviors { get; } + public IReadOnlyList BackToWindowsButtonDisplayModes { get; } + + public IReadOnlyList BackToWindowsIconSources { get; } + + public IReadOnlyList BackToWindowsFluentIcons { get; } + public IReadOnlyList TimeZones { get; } [ObservableProperty] @@ -336,6 +395,24 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo private SelectionOption _selectedMultiInstanceLaunchBehavior = new(MultiInstanceLaunchBehavior.NotifyAndOpenDesktop.ToString(), "Notify and open desktop"); + [ObservableProperty] + private SelectionOption _selectedBackToWindowsButtonDisplayMode = new("IconAndText", "Icon and text"); + + [ObservableProperty] + private SelectionOption _selectedBackToWindowsIconSource = new("FluentIcon", "Fluent icon"); + + [ObservableProperty] + private FluentIconSelectionOption _selectedBackToWindowsFluentIcon = new("Circle", "Circle", Icon.Circle); + + [ObservableProperty] + private IReadOnlyList _filteredBackToWindowsFluentIcons = []; + + [ObservableProperty] + private string _backToWindowsFluentIconSearchText = string.Empty; + + [ObservableProperty] + private string _backToWindowsIconText = "○"; + [ObservableProperty] private bool _enableFadeTransition = true; @@ -366,6 +443,39 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo [ObservableProperty] private string _multiInstanceLaunchBehaviorDescription = string.Empty; + [ObservableProperty] + private string _backToWindowsButtonDisplayModeHeader = string.Empty; + + [ObservableProperty] + private string _backToWindowsButtonDisplayModeDescription = string.Empty; + + [ObservableProperty] + private string _backToWindowsIconSourceHeader = string.Empty; + + [ObservableProperty] + private string _backToWindowsIconSourceDescription = string.Empty; + + [ObservableProperty] + private string _backToWindowsFluentIconHeader = string.Empty; + + [ObservableProperty] + private string _backToWindowsFluentIconDescription = string.Empty; + + [ObservableProperty] + private string _backToWindowsFluentIconSearchPlaceholder = string.Empty; + + [ObservableProperty] + private string _backToWindowsIconTextHeader = string.Empty; + + [ObservableProperty] + private string _backToWindowsIconTextDescription = string.Empty; + + public bool IsBackToWindowsFluentIconSource => + string.Equals(SelectedBackToWindowsIconSource?.Value, "FluentIcon", StringComparison.OrdinalIgnoreCase); + + public bool IsBackToWindowsTextIconSource => + string.Equals(SelectedBackToWindowsIconSource?.Value, "Text", StringComparison.OrdinalIgnoreCase); + public bool IsSlideTransitionAvailable => System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); public bool IsFadeTransitionToggleEnabled => !EnableSlideTransition; @@ -488,6 +598,69 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo SaveField(nameof(AppSettingsSnapshot.MultiInstanceLaunchBehavior), behavior); } + partial void OnSelectedBackToWindowsButtonDisplayModeChanged(SelectionOption value) + { + if (_isInitializing || value is null) + { + return; + } + + SaveField( + nameof(AppSettingsSnapshot.BackToWindowsButtonDisplayMode), + NormalizeBackToWindowsButtonDisplayMode(value.Value)); + } + + partial void OnSelectedBackToWindowsIconSourceChanged(SelectionOption value) + { + OnPropertyChanged(nameof(IsBackToWindowsFluentIconSource)); + OnPropertyChanged(nameof(IsBackToWindowsTextIconSource)); + + if (_isInitializing || value is null) + { + return; + } + + SaveField( + nameof(AppSettingsSnapshot.BackToWindowsIconSource), + NormalizeBackToWindowsIconSource(value.Value)); + } + + partial void OnSelectedBackToWindowsFluentIconChanged(FluentIconSelectionOption value) + { + if (_isInitializing || value is null) + { + return; + } + + SaveField( + nameof(AppSettingsSnapshot.BackToWindowsFluentIconName), + NormalizeBackToWindowsFluentIconName(value.Value)); + } + + partial void OnBackToWindowsFluentIconSearchTextChanged(string value) + { + var query = value?.Trim() ?? string.Empty; + FilteredBackToWindowsFluentIcons = string.IsNullOrWhiteSpace(query) + ? BackToWindowsFluentIcons + : BackToWindowsFluentIcons + .Where(option => option.Label.Contains(query, StringComparison.OrdinalIgnoreCase) || + option.Value.Contains(query, StringComparison.OrdinalIgnoreCase)) + .Take(120) + .ToList(); + } + + partial void OnBackToWindowsIconTextChanged(string value) + { + if (_isInitializing) + { + return; + } + + SaveField( + nameof(AppSettingsSnapshot.BackToWindowsIconText), + NormalizeBackToWindowsIconText(value)); + } + partial void OnEnableSlideTransitionChanged(bool value) { if (_isInitializing) @@ -597,6 +770,86 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo ]; } + private IReadOnlyList CreateBackToWindowsButtonDisplayModeOptions() + { + return + [ + new SelectionOption( + "IconAndText", + L("settings.general.back_to_windows_button_display.icon_and_text", "Icon and text")), + new SelectionOption( + "IconOnly", + L("settings.general.back_to_windows_button_display.icon_only", "Icon only")), + new SelectionOption( + "TextOnly", + L("settings.general.back_to_windows_button_display.text_only", "Text only")) + ]; + } + + private IReadOnlyList CreateBackToWindowsIconSourceOptions() + { + return + [ + new SelectionOption( + "FluentIcon", + L("settings.general.back_to_windows_icon_source.fluent_icon", "Fluent icon")), + new SelectionOption( + "Text", + L("settings.general.back_to_windows_icon_source.text", "Text icon")) + ]; + } + + private IReadOnlyList CreateBackToWindowsFluentIconOptions() + { + return Enum.GetValues() + .Select(icon => icon.ToString()) + .Order(StringComparer.OrdinalIgnoreCase) + .Select(name => new FluentIconSelectionOption(name, name, Enum.Parse(name))) + .ToList(); + } + + private static string NormalizeBackToWindowsButtonDisplayMode(string? value) + { + return value switch + { + _ when string.Equals(value, "IconOnly", StringComparison.OrdinalIgnoreCase) => "IconOnly", + _ when string.Equals(value, "TextOnly", StringComparison.OrdinalIgnoreCase) => "TextOnly", + _ => "IconAndText" + }; + } + + private static string NormalizeBackToWindowsIconSource(string? value) + { + return string.Equals(value, "Text", StringComparison.OrdinalIgnoreCase) + ? "Text" + : "FluentIcon"; + } + + private static string NormalizeBackToWindowsFluentIconName(string? value) + { + return Enum.TryParse(value, ignoreCase: true, out var icon) + ? icon.ToString() + : Icon.Circle.ToString(); + } + + private static string NormalizeBackToWindowsIconText(string? value) + { + var normalized = string.IsNullOrWhiteSpace(value) + ? "○" + : value.Trim(); + + var enumerator = StringInfo.GetTextElementEnumerator(normalized); + var builder = new System.Text.StringBuilder(); + var count = 0; + while (enumerator.MoveNext() && count < 4) + { + builder.Append(enumerator.GetTextElement()); + count++; + } + + return builder.Length > 0 ? builder.ToString() : "○"; + } + private IReadOnlyList CreateTimeZoneOptions() { return _timeZoneService @@ -642,6 +895,33 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo MultiInstanceLaunchBehaviorDescription = L( "settings.general.multi_instance_behavior_desc", "Choose how Launcher handles repeated launches while LanMountain Desktop is already running."); + BackToWindowsButtonDisplayModeHeader = L( + "settings.general.back_to_windows_button_display_header", + "Back to platform button"); + BackToWindowsButtonDisplayModeDescription = L( + "settings.general.back_to_windows_button_display_desc", + "Choose whether the Dock button shows its circle icon, text, or both."); + BackToWindowsIconSourceHeader = L( + "settings.general.back_to_windows_icon_source_header", + "Back button icon source"); + BackToWindowsIconSourceDescription = L( + "settings.general.back_to_windows_icon_source_desc", + "Choose whether the left icon slot uses a Fluent icon or short custom text."); + BackToWindowsFluentIconHeader = L( + "settings.general.back_to_windows_fluent_icon_header", + "Fluent icon"); + BackToWindowsFluentIconDescription = L( + "settings.general.back_to_windows_fluent_icon_desc", + "Search and choose a built-in Fluent icon for the left icon slot."); + BackToWindowsFluentIconSearchPlaceholder = L( + "settings.general.back_to_windows_fluent_icon_search_placeholder", + "Search icon"); + BackToWindowsIconTextHeader = L( + "settings.general.back_to_windows_icon_text_header", + "Text icon"); + BackToWindowsIconTextDescription = L( + "settings.general.back_to_windows_icon_text_desc", + "Enter up to four characters to display as the left icon."); } private void RefreshPreview() diff --git a/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs index 12315bd..d489f02 100644 --- a/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs +++ b/LanMountainDesktop/ViewModels/WeatherSettingsPageViewModel.cs @@ -21,6 +21,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase private readonly WeatherLocationRefreshService _weatherLocationRefreshService; private string _languageCode; private bool _isInitializing; + private WeatherSnapshot? _previewSnapshot; public WeatherSettingsPageViewModel( ISettingsFacadeService settingsFacade, @@ -39,6 +40,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase RefreshLocalizedText(); LocationModes = CreateLocationModes(); + VisualStyles = CreateVisualStyles(); var weatherState = _settingsFacade.Weather.Get(); _isInitializing = true; @@ -60,6 +62,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase public IReadOnlyList LocationModes { get; } + public IReadOnlyList VisualStyles { get; } + public ObservableCollection SearchResults { get; } = []; [ObservableProperty] @@ -98,6 +102,12 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase [ObservableProperty] private string _locationServicesDescription = string.Empty; + [ObservableProperty] + private string _visualStyleHeader = string.Empty; + + [ObservableProperty] + private string _visualStyleDescription = string.Empty; + [ObservableProperty] private string _alertFilterHeader = string.Empty; @@ -161,6 +171,9 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase [ObservableProperty] private SelectionOption _selectedLocationMode = new("CitySearch", "City Search"); + [ObservableProperty] + private SelectionOption _selectedVisualStyle = new(WeatherVisualStyleId.Default, "Google Weather v4"); + [ObservableProperty] private bool _isCitySearchMode = true; @@ -246,6 +259,17 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase _ = RefreshPreviewAsync(); } + partial void OnSelectedVisualStyleChanged(SelectionOption value) + { + if (_isInitializing || value is null) + { + return; + } + + _settingsFacade.Weather.Save(CreateEditableState()); + UpdatePreviewIcon(_previewSnapshot); + } + partial void OnAutoRefreshLocationChanged(bool value) { _ = value; @@ -345,7 +369,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase selected.Longitude, AutoRefreshLocation, ExcludedAlerts ?? string.Empty, - "DefaultWeather", + SelectedVisualStyle?.Value ?? WeatherVisualStyleId.Default, NoTlsRequests, SearchKeyword?.Trim() ?? string.Empty); @@ -417,7 +441,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase if (string.IsNullOrWhiteSpace(state.LocationKey)) { PreviewStatus = L("settings.weather.preview_missing_location", "Please apply one weather location before testing."); - PreviewIcon = null; + _previewSnapshot = null; + UpdatePreviewIcon(null); PreviewLocation = CurrentLocationSummary; PreviewTemperature = "--"; PreviewCondition = string.Empty; @@ -439,12 +464,14 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase ResolveCulture(), L("settings.weather.preview_failed_format", "Test fetch failed: {0}"), result.ErrorMessage ?? result.ErrorCode ?? L("settings.weather.preview_unknown", "Unknown")); - PreviewIcon = null; + _previewSnapshot = null; + UpdatePreviewIcon(null); return; } var snapshot = result.Data; - PreviewIcon = null; + _previewSnapshot = snapshot; + UpdatePreviewIcon(snapshot); PreviewLocation = string.IsNullOrWhiteSpace(snapshot.LocationName) ? state.LocationName : snapshot.LocationName!; @@ -517,6 +544,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase SearchStatus = "2 sample locations are shown for design preview."; LocationActionStatus = "Using mocked Windows location support in design mode."; PreviewIcon = null; + UpdatePreviewIcon(null); PreviewLocation = previewLocation.Name; PreviewTemperature = "24 deg C"; PreviewCondition = ResolveWeatherDisplayText("Partly cloudy", 4); @@ -538,6 +566,8 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase CoordinatesDescription = L("settings.weather.coordinates_desc", "Set latitude/longitude and optional key/name."); LocationServicesHeader = L("settings.weather.location_services_header", "Location Service"); LocationServicesDescription = L("settings.weather.location_services_desc", "Use the current Windows location and decide whether it refreshes automatically at startup."); + VisualStyleHeader = L("settings.weather.visual_style_header", "Weather Visual Style"); + VisualStyleDescription = L("settings.weather.visual_style_desc", "Choose the icon and component style used by desktop weather widgets."); AlertFilterHeader = L("settings.weather.alert_filter_header", "Excluded Alerts"); AlertFilterDescription = L("settings.weather.alert_filter_desc", "Alerts containing these words will not be shown. One rule per line."); RequestHeader = L("settings.weather.no_tls_header", "No TLS Weather Request"); @@ -569,6 +599,13 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase ]; } + private IReadOnlyList CreateVisualStyles() + { + return WeatherVisualStyleCatalog.GetStyles() + .Select(style => new SelectionOption(style.Id, L($"settings.weather.visual_style.{style.Id}", style.DisplayName))) + .ToArray(); + } + private void UpdateModeVisibility() { var mode = SelectedLocationMode?.Value ?? "CitySearch"; @@ -629,7 +666,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase Longitude, AutoRefreshLocation, ExcludedAlerts ?? string.Empty, - "DefaultWeather", + SelectedVisualStyle?.Value ?? WeatherVisualStyleId.Default, NoTlsRequests, SearchKeyword?.Trim() ?? string.Empty); } @@ -646,7 +683,7 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase SelectedSearchResult.Longitude, AutoRefreshLocation, ExcludedAlerts ?? string.Empty, - "DefaultWeather", + SelectedVisualStyle?.Value ?? WeatherVisualStyleId.Default, NoTlsRequests, SearchKeyword?.Trim() ?? string.Empty); } @@ -665,6 +702,9 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase SelectedLocationMode = LocationModes.FirstOrDefault(option => string.Equals(option.Value, state.LocationMode, StringComparison.OrdinalIgnoreCase)) ?? LocationModes[0]; + SelectedVisualStyle = VisualStyles.FirstOrDefault(option => + string.Equals(option.Value, WeatherVisualStyleCatalog.Normalize(state.IconPackId), StringComparison.OrdinalIgnoreCase)) + ?? VisualStyles[0]; Latitude = state.Latitude; Longitude = state.Longitude; LocationKey = state.LocationKey; @@ -698,6 +738,14 @@ public sealed partial class WeatherSettingsPageViewModel : ViewModelBase return _localizationService.GetString(_languageCode, key, fallback); } + private void UpdatePreviewIcon(WeatherSnapshot? snapshot) + { + var styleId = SelectedVisualStyle?.Value ?? WeatherVisualStyleId.Default; + PreviewIcon = snapshot is null + ? WeatherIconAssetResolver.LoadIcon(styleId, 1, "Partly cloudy") + : WeatherIconAssetResolver.LoadIcon(styleId, snapshot); + } + private string ResolveWeatherDisplayText(string? weatherText, int? weatherCode) { if (!string.IsNullOrWhiteSpace(weatherText)) diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs index e80bbf0..77002a0 100644 --- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs +++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs @@ -332,6 +332,10 @@ public sealed class DesktopComponentRuntimeRegistry BuiltInComponentIds.DesktopClock, "component.desktop_clock", () => new AnalogClockWidget()), + new DesktopComponentRuntimeRegistration( + BuiltInComponentIds.DesktopWeatherClock, + "component.weather_clock", + () => new WeatherClockWidget()), new DesktopComponentRuntimeRegistration( BuiltInComponentIds.DesktopWorldClock, "component.world_clock", @@ -340,6 +344,22 @@ public sealed class DesktopComponentRuntimeRegistry BuiltInComponentIds.DesktopTimer, "component.desktop_timer", () => new TimerWidget()), + new DesktopComponentRuntimeRegistration( + BuiltInComponentIds.DesktopWeather, + "component.desktop_weather", + () => new WeatherWidget()), + new DesktopComponentRuntimeRegistration( + BuiltInComponentIds.DesktopHourlyWeather, + "component.hourly_weather", + () => new HourlyWeatherWidget()), + new DesktopComponentRuntimeRegistration( + BuiltInComponentIds.DesktopMultiDayWeather, + "component.multiday_weather", + () => new MultiDayWeatherWidget()), + new DesktopComponentRuntimeRegistration( + BuiltInComponentIds.DesktopExtendedWeather, + "component.extended_weather", + () => new ExtendedWeatherWidget()), new DesktopComponentRuntimeRegistration( BuiltInComponentIds.DesktopClassSchedule, "component.class_schedule", diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml new file mode 100644 index 0000000..f6dd17b --- /dev/null +++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs new file mode 100644 index 0000000..e7526fa --- /dev/null +++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs @@ -0,0 +1,118 @@ +using System.Linq; +using Avalonia.Controls; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.Views.Components; + +public partial class ExtendedWeatherWidget : WeatherWidgetBase +{ + public ExtendedWeatherWidget() + { + InitializeComponent(); + RenderWeather(); + } + + protected override MaterialWeatherSceneControl SceneControl => Scene; + + protected override void ApplyResponsiveLayout(double cellSize) + { + var scale = cellSize / 64d; + ContentGrid.Margin = new Avalonia.Thickness(18 * scale, 14 * scale, 18 * scale, 12 * scale); + ContentGrid.RowSpacing = 10 * scale; + LocationTextBlock.FontSize = System.Math.Clamp(12 * scale, 9, 18); + ConditionTextBlock.FontSize = System.Math.Clamp(15 * scale, 11, 22); + TemperatureTextBlock.FontSize = System.Math.Clamp(52 * scale, 32, 76); + MainIcon.Width = System.Math.Clamp(48 * scale, 32, 72); + MainIcon.Height = System.Math.Clamp(48 * scale, 32, 72); + } + + protected override void RenderWeather() + { + RootBorder.Background = Brush(CurrentPalette.BackgroundBottom); + OverlayBorder.Background = new Avalonia.Media.SolidColorBrush(CurrentPalette.OverlayTint); + LocationTextBlock.Foreground = Brush(CurrentPalette.TextSecondary, 0.78); + ConditionTextBlock.Foreground = Brush(CurrentPalette.TextPrimary, 0.9); + TemperatureTextBlock.Foreground = Brush(CurrentPalette.TextPrimary); + LocationTextBlock.Text = DisplayLocation; + ConditionTextBlock.Text = State == WeatherWidgetState.Error ? "Weather unavailable" : MaterialWeatherVisualTheme.ResolveDisplayText(Snapshot, StatusText); + TemperatureTextBlock.Text = FormatTemperature(Snapshot?.Current.TemperatureC); + MainIcon.SetWeatherIcon(CurrentVisualStyleId, Snapshot); + BuildMetrics(); + BuildHourlyItems(); + BuildDailyItems(); + } + + private void BuildMetrics() + { + MetricGrid.Children.Clear(); + MetricGrid.Children.Add(CreateMetric("AQI", Snapshot?.Current.AirQualityIndex?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "--")); + MetricGrid.Children.Add(CreateMetric("Humidity", Snapshot?.Current.RelativeHumidityPercent is int h ? $"{h}%" : "--")); + MetricGrid.Children.Add(CreateMetric("Wind", Snapshot?.Current.WindSpeedKph is double w ? $"{w:0.#} km/h" : "--")); + } + + private Control CreateMetric(string label, string value) + { + var surfaceBrush = new Avalonia.Media.SolidColorBrush(CurrentPalette.SurfaceColor); + var panel = new StackPanel { Spacing = 3, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }; + panel.Children.Add(new Border + { + Background = surfaceBrush, + CornerRadius = new Avalonia.CornerRadius(6), + Padding = new Avalonia.Thickness(8, 5), + Child = new StackPanel + { + Spacing = 2, + Children = + { + new TextBlock { Text = value, Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 12 }, + new TextBlock { Text = label, FontSize = 10, Foreground = Brush(CurrentPalette.TextSecondary), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center } + } + } + }); + return panel; + } + + private void BuildHourlyItems() + { + HourlyGrid.Children.Clear(); + var forecasts = Snapshot?.HourlyForecasts.Take(6).ToArray() ?? CreatePreviewSnapshot().HourlyForecasts.Take(6).ToArray(); + foreach (var item in forecasts) + { + var surfaceBrush = new Avalonia.Media.SolidColorBrush(CurrentPalette.SurfaceVariantColor); + var panel = new Border + { + Background = surfaceBrush, + CornerRadius = new Avalonia.CornerRadius(6), + Padding = new Avalonia.Thickness(5, 5), + Child = new StackPanel { Spacing = 2, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center } + }; + var inner = (StackPanel)panel.Child!; + inner.Children.Add(new TextBlock { Text = FormatTime(item.Time), Foreground = Brush(CurrentPalette.TextSecondary), FontSize = 10, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }); + inner.Children.Add(new WeatherIconView { Width = 26, Height = 26, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.WeatherCode, item.WeatherText) }); + inner.Children.Add(new TextBlock { Text = FormatTemperature(item.TemperatureC), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 12 }); + HourlyGrid.Children.Add(panel); + } + } + + private void BuildDailyItems() + { + DailyGrid.Children.Clear(); + var forecasts = Snapshot?.DailyForecasts.Take(5).ToArray() ?? CreatePreviewSnapshot().DailyForecasts.Take(5).ToArray(); + foreach (var item in forecasts) + { + var surfaceBrush = new Avalonia.Media.SolidColorBrush(CurrentPalette.SurfaceVariantColor); + var panel = new Border + { + Background = surfaceBrush, + CornerRadius = new Avalonia.CornerRadius(6), + Padding = new Avalonia.Thickness(5, 5), + Child = new StackPanel { Spacing = 2, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center } + }; + var inner = (StackPanel)panel.Child!; + inner.Children.Add(new TextBlock { Text = ResolveDayLabel(item.Date), Foreground = Brush(CurrentPalette.TextSecondary), FontSize = 10, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }); + inner.Children.Add(new WeatherIconView { Width = 26, Height = 26, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.DayWeatherCode, item.DayWeatherText) }); + inner.Children.Add(new TextBlock { Text = $"{FormatTemperature(item.HighTemperatureC)} / {FormatTemperature(item.LowTemperatureC)}", Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 11 }); + DailyGrid.Children.Add(panel); + } + } +} diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml new file mode 100644 index 0000000..7c7f23f --- /dev/null +++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs new file mode 100644 index 0000000..e20f11b --- /dev/null +++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs @@ -0,0 +1,81 @@ +using System.Linq; +using Avalonia.Controls; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.Views.Components; + +public partial class HourlyWeatherWidget : WeatherWidgetBase +{ + public HourlyWeatherWidget() + { + InitializeComponent(); + RenderWeather(); + } + + protected override MaterialWeatherSceneControl SceneControl => Scene; + + protected override void ApplyResponsiveLayout(double cellSize) + { + var scale = cellSize / 64d; + ContentGrid.Margin = new Avalonia.Thickness(16 * scale, 12 * scale); + ContentGrid.RowSpacing = 10 * scale; + TemperatureTextBlock.FontSize = System.Math.Clamp(40 * scale, 26, 58); + ConditionTextBlock.FontSize = System.Math.Clamp(14 * scale, 10, 20); + LocationTextBlock.FontSize = System.Math.Clamp(11 * scale, 8, 16); + RangeTextBlock.FontSize = System.Math.Clamp(11 * scale, 8, 16); + MainIcon.Width = System.Math.Clamp(44 * scale, 28, 60); + MainIcon.Height = System.Math.Clamp(44 * scale, 28, 60); + } + + protected override void RenderWeather() + { + RootBorder.Background = Brush(CurrentPalette.BackgroundBottom); + OverlayBorder.Background = new Avalonia.Media.SolidColorBrush(CurrentPalette.OverlayTint); + TemperatureTextBlock.Foreground = Brush(CurrentPalette.TextPrimary); + ConditionTextBlock.Foreground = Brush(CurrentPalette.TextPrimary, 0.88); + LocationTextBlock.Foreground = Brush(CurrentPalette.TextSecondary, 0.78); + RangeTextBlock.Foreground = Brush(CurrentPalette.TextSecondary); + TemperatureTextBlock.Text = FormatTemperature(Snapshot?.Current.TemperatureC); + MainIcon.SetWeatherIcon(CurrentVisualStyleId, Snapshot); + ConditionTextBlock.Text = State == WeatherWidgetState.Error ? "Weather unavailable" : MaterialWeatherVisualTheme.ResolveDisplayText(Snapshot, StatusText); + LocationTextBlock.Text = DisplayLocation; + RangeTextBlock.Text = FormatRange(Snapshot); + BuildHourlyItems(); + } + + private void BuildHourlyItems() + { + HourlyGrid.Children.Clear(); + var items = (Snapshot?.HourlyForecasts.Take(6).ToArray() ?? CreatePreviewSnapshot().HourlyForecasts.Take(6).ToArray()) + .Select(item => new WeatherChipItem( + FormatTime(item.Time), + FormatTemperature(item.TemperatureC), + item.WeatherCode, + item.WeatherText, + MaterialWeatherVisualTheme.ResolveCondition(item.WeatherCode, item.WeatherText))) + .ToArray(); + foreach (var item in items) + { + HourlyGrid.Children.Add(CreateChip(item)); + } + } + + private Control CreateChip(WeatherChipItem item) + { + var surfaceBrush = new Avalonia.Media.SolidColorBrush(CurrentPalette.SurfaceVariantColor); + var panel = new Border + { + Background = surfaceBrush, + CornerRadius = new Avalonia.CornerRadius(6), + Padding = new Avalonia.Thickness(5, 5), + Child = new StackPanel { Spacing = 2, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center } + }; + var inner = (StackPanel)panel.Child!; + inner.Children.Add(new TextBlock { Text = item.Label, FontSize = 10, Foreground = Brush(CurrentPalette.TextSecondary), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextAlignment = Avalonia.Media.TextAlignment.Center }); + inner.Children.Add(new WeatherIconView { Width = 24, Height = 24, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.WeatherCode, item.WeatherText) }); + inner.Children.Add(new TextBlock { Text = item.Value, FontWeight = Avalonia.Media.FontWeight.SemiBold, Foreground = Brush(CurrentPalette.TextPrimary), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 11, TextAlignment = Avalonia.Media.TextAlignment.Center }); + return panel; + } + + private readonly record struct WeatherChipItem(string Label, string Value, int? WeatherCode, string? WeatherText, MaterialWeatherCondition Condition); +} diff --git a/LanMountainDesktop/Views/Components/MaterialWeatherSceneControl.cs b/LanMountainDesktop/Views/Components/MaterialWeatherSceneControl.cs new file mode 100644 index 0000000..7198252 --- /dev/null +++ b/LanMountainDesktop/Views/Components/MaterialWeatherSceneControl.cs @@ -0,0 +1,382 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.Views.Components; + +public sealed class MaterialWeatherSceneControl : Control +{ + private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(66) }; + private MaterialWeatherPalette _palette = MaterialWeatherVisualTheme.ResolvePalette(MaterialWeatherCondition.Clear, false); + private MaterialWeatherCondition _condition = MaterialWeatherCondition.Clear; + private string _styleId = WeatherVisualStyleId.Default; + private double _phase; + private bool _isLive; + private bool _isAttached; + + private static readonly Random _rng = new(42); + + public MaterialWeatherSceneControl() + { + IsHitTestVisible = false; + _timer.Tick += (_, _) => + { + _phase = (_phase + 0.008) % 1d; + InvalidateVisual(); + }; + } + + public void Apply(string? styleId, MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive) + { + _styleId = WeatherVisualStyleCatalog.Normalize(styleId); + _condition = condition; + _palette = palette; + _isLive = isLive; + UpdateTimer(); + InvalidateVisual(); + } + + public void Apply(MaterialWeatherCondition condition, MaterialWeatherPalette palette, bool isLive) + { + Apply(_styleId, condition, palette, isLive); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _isAttached = false; + _timer.Stop(); + base.OnDetachedFromVisualTree(e); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _isAttached = true; + UpdateTimer(); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + var rect = new Rect(Bounds.Size); + if (rect.Width <= 1 || rect.Height <= 1) return; + + context.DrawRectangle(CreateLinearBrush(_palette.BackgroundTop, _palette.BackgroundBottom, 0, 0, 1, 1), null, rect); + + using (context.PushClip(rect)) + { + DrawStyleDecoration(context, rect); + + switch (_condition) + { + case MaterialWeatherCondition.Rain: + case MaterialWeatherCondition.Storm: + DrawRain(context, rect, _condition == MaterialWeatherCondition.Storm); + break; + case MaterialWeatherCondition.Snow: + DrawSnow(context, rect); + break; + case MaterialWeatherCondition.Fog: + case MaterialWeatherCondition.Haze: + DrawFog(context, rect); + break; + } + } + } + + private void UpdateTimer() + { + if (_isLive && _isAttached) _timer.Start(); + else _timer.Stop(); + } + + private void DrawStyleDecoration(DrawingContext ctx, Rect r) + { + var t = Math.Sin(_phase * Math.PI * 2d); + switch (_styleId) + { + case WeatherVisualStyleId.Geometric: + DrawGeometricDecoration(ctx, r, t); + break; + case WeatherVisualStyleId.Breezy: + DrawBreezyDecoration(ctx, r, t); + break; + case WeatherVisualStyleId.LemonFlutter: + DrawLemonDecoration(ctx, r, t); + break; + } + } + + private void DrawGeometricDecoration(DrawingContext ctx, Rect r, double t) + { + var min = Math.Min(r.Width, r.Height); + + DrawRadialGlow(ctx, r.Width * 0.78 + t * 6, r.Height * 0.20 + t * 4, min * 0.55, _palette.PrimaryShape, 0.22, 0.0); + DrawRadialGlow(ctx, r.Width * 0.12 - t * 4, r.Height * 0.68 + t * 3, min * 0.42, _palette.SecondaryShape, 0.18, 0.0); + DrawRadialGlow(ctx, r.Width * 0.52, r.Height * 0.82 - t * 5, min * 0.32, _palette.AccentShape, 0.14, 0.0); + + DrawRadialGlow(ctx, r.Width * 0.35 + t * 3, r.Height * 0.12, min * 0.28, _palette.AccentShape, 0.08, 0.0); + DrawRadialGlow(ctx, r.Width * 0.88 - t * 2, r.Height * 0.55, min * 0.22, _palette.PrimaryShape, 0.10, 0.0); + + DrawArcSegment(ctx, r.Width * 0.65 + t * 4, r.Height * 0.35, min * 0.38, -30, 120, _palette.SecondaryShape, 0.12, 2.5); + DrawArcSegment(ctx, r.Width * 0.25 - t * 3, r.Height * 0.50, min * 0.30, 45, 90, _palette.AccentShape, 0.10, 2); + } + + private void DrawBreezyDecoration(DrawingContext ctx, Rect r, double t) + { + var min = Math.Min(r.Width, r.Height); + + DrawRadialGlow(ctx, r.Width * 0.72 + t * 5, r.Height * 0.25 + t * 3, min * 0.48, _palette.PrimaryShape, 0.20, 0.0); + DrawRadialGlow(ctx, r.Width * 0.20 - t * 4, r.Height * 0.60 + t * 4, min * 0.36, _palette.SecondaryShape, 0.16, 0.0); + DrawRadialGlow(ctx, r.Width * 0.50, r.Height * 0.80 - t * 3, min * 0.28, _palette.AccentShape, 0.12, 0.0); + + for (var i = 0; i < 4; i++) + { + var y = r.Height * (0.25 + i * 0.18); + var shift = Math.Sin(_phase * Math.PI * 2 + i * 1.1) * r.Width * 0.05; + DrawWaveLine(ctx, r, y, shift, i, _palette.SurfaceTint, 0.10 + i * 0.02); + } + + DrawArcSegment(ctx, r.Width * 0.80 + t * 3, r.Height * 0.15, min * 0.25, 0, 180, _palette.PrimaryShape, 0.08, 1.5); + DrawArcSegment(ctx, r.Width * 0.15 - t * 2, r.Height * 0.75, min * 0.20, 90, 180, _palette.AccentShape, 0.08, 1.5); + } + + private void DrawLemonDecoration(DrawingContext ctx, Rect r, double t) + { + var min = Math.Min(r.Width, r.Height); + + switch (_condition) + { + case MaterialWeatherCondition.Clear: + case MaterialWeatherCondition.PartlyCloudy: + case MaterialWeatherCondition.Unknown: + DrawSunScene(ctx, r, min, t); + break; + case MaterialWeatherCondition.Cloudy: + DrawCloudScene(ctx, r, min, t); + break; + case MaterialWeatherCondition.Rain: + case MaterialWeatherCondition.Storm: + DrawRainScene(ctx, r, min, t); + break; + case MaterialWeatherCondition.Snow: + DrawSnowScene(ctx, r, min, t); + break; + default: + DrawSunScene(ctx, r, min, t); + break; + } + + DrawRadialGlow(ctx, r.Width * 0.15 - t * 3, r.Height * 0.70 + t * 4, min * 0.30, _palette.SecondaryShape, 0.10, 0.0); + DrawRadialGlow(ctx, r.Width * 0.85 + t * 2, r.Height * 0.55 - t * 3, min * 0.22, _palette.AccentShape, 0.08, 0.0); + } + + private void DrawSunScene(DrawingContext ctx, Rect r, double min, double t) + { + var cx = r.Width * 0.70; + var cy = r.Height * 0.25; + + DrawRadialGlow(ctx, cx, cy, min * 0.35, _palette.PrimaryShape, 0.28, 0.0); + DrawRadialGlow(ctx, cx, cy, min * 0.18, _palette.PrimaryShape, 0.45, 0.10); + + var rayCount = 14; + var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.18), Math.Max(2, min * 0.012), lineCap: PenLineCap.Round); + for (var i = 0; i < rayCount; i++) + { + var angle = (i / (double)rayCount) * Math.PI * 2 + t * 0.25; + var innerR = min * 0.16; + var outerR = min * 0.30 + Math.Sin(angle * 3 + t * 2) * min * 0.04; + ctx.DrawLine(pen, + new Point(cx + Math.Cos(angle) * innerR, cy + Math.Sin(angle) * innerR), + new Point(cx + Math.Cos(angle) * outerR, cy + Math.Sin(angle) * outerR)); + } + } + + private void DrawCloudScene(DrawingContext ctx, Rect r, double min, double t) + { + DrawRadialGlow(ctx, r.Width * 0.60 + t * 5, r.Height * 0.30, min * 0.40, _palette.PrimaryShape, 0.16, 0.0); + DrawRadialGlow(ctx, r.Width * 0.35 - t * 3, r.Height * 0.55, min * 0.32, _palette.SecondaryShape, 0.12, 0.0); + + var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.14), Math.Max(1.5, min * 0.010), lineCap: PenLineCap.Round); + var drift = t * 6; + + DrawCloudOutline(ctx, r.Width * 0.42 + drift, r.Height * 0.32, min * 0.18, min * 0.12, pen); + DrawCloudOutline(ctx, r.Width * 0.58 + drift * 0.7, r.Height * 0.26, min * 0.22, min * 0.15, pen); + DrawCloudOutline(ctx, r.Width * 0.72 + drift * 0.5, r.Height * 0.35, min * 0.14, min * 0.10, pen); + } + + private void DrawRainScene(DrawingContext ctx, Rect r, double min, double t) + { + DrawRadialGlow(ctx, r.Width * 0.65 + t * 4, r.Height * 0.25, min * 0.38, _palette.PrimaryShape, 0.14, 0.0); + DrawRadialGlow(ctx, r.Width * 0.30 - t * 3, r.Height * 0.50, min * 0.30, _palette.SecondaryShape, 0.10, 0.0); + + var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.10), Math.Max(1, r.Width / 200), lineCap: PenLineCap.Round); + var streaks = Math.Clamp((int)(r.Width / 28), 6, 16); + for (var i = 0; i < streaks; i++) + { + var progress = (_phase * 0.5 + i * 0.12) % 1d; + var x = r.Width * (0.12 + (i % streaks) / (double)streaks * 0.78); + var y = r.Height * (0.15 + progress * 0.75); + var len = r.Height * 0.08; + ctx.DrawLine(pen, new Point(x, y), new Point(x - r.Width * 0.018, y + len)); + } + } + + private void DrawSnowScene(DrawingContext ctx, Rect r, double min, double t) + { + DrawRadialGlow(ctx, r.Width * 0.68 + t * 3, r.Height * 0.22, min * 0.35, _palette.PrimaryShape, 0.16, 0.0); + DrawRadialGlow(ctx, r.Width * 0.25 - t * 2, r.Height * 0.55, min * 0.28, _palette.AccentShape, 0.10, 0.0); + + var cx = r.Width * 0.72; + var cy = r.Height * 0.28; + var sr = min * 0.12; + var pen = new Pen(new SolidColorBrush(_palette.PrimaryShape, 0.16), Math.Max(1.2, min * 0.008), lineCap: PenLineCap.Round); + for (var i = 0; i < 6; i++) + { + var a = (i / 6d) * Math.PI * 2 + t * 0.15; + var ex = cx + Math.Cos(a) * sr; + var ey = cy + Math.Sin(a) * sr; + ctx.DrawLine(pen, new Point(cx, cy), new Point(ex, ey)); + var br = sr * 0.35; + var mx = cx + Math.Cos(a) * sr * 0.6; + var my = cy + Math.Sin(a) * sr * 0.6; + ctx.DrawLine(pen, new Point(mx, my), new Point(mx + Math.Cos(a + 0.5) * br, my + Math.Sin(a + 0.5) * br)); + ctx.DrawLine(pen, new Point(mx, my), new Point(mx + Math.Cos(a - 0.5) * br, my + Math.Sin(a - 0.5) * br)); + } + } + + private void DrawRadialGlow(DrawingContext ctx, double cx, double cy, double radius, Color baseColor, double peakAlpha, double centerBoost) + { + if (radius < 1) return; + + var peak = (byte)Math.Clamp(peakAlpha * 255, 0, 255); + var edge = (byte)0; + var center = (byte)Math.Clamp(centerBoost * 255, 0, 255); + + var brush = new RadialGradientBrush + { + Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + GradientStops = + { + new GradientStop(new Color(Math.Clamp((byte)(peak + center), (byte)0, (byte)255), baseColor.R, baseColor.G, baseColor.B), 0), + new GradientStop(new Color((byte)(peak * 0.6), baseColor.R, baseColor.G, baseColor.B), 0.4), + new GradientStop(new Color(edge, baseColor.R, baseColor.G, baseColor.B), 1) + } + }; + + ctx.DrawEllipse(brush, null, new Point(cx, cy), radius, radius); + } + + private void DrawArcSegment(DrawingContext ctx, double cx, double cy, double radius, double startDeg, double sweepDeg, Color color, double alpha, double thickness) + { + if (radius < 2) return; + + var pen = new Pen(new SolidColorBrush(color, (float)alpha), thickness, lineCap: PenLineCap.Round); + + var stream = new StreamGeometry(); + var g = stream.Open(); + + var startRad = startDeg * Math.PI / 180d; + var sweepRad = sweepDeg * Math.PI / 180d; + var steps = Math.Max(8, (int)(sweepDeg / 5)); + + g.BeginFigure(new Point(cx + Math.Cos(startRad) * radius, cy + Math.Sin(startRad) * radius), false); + for (var i = 1; i <= steps; i++) + { + var a = startRad + sweepRad * (i / (double)steps); + g.LineTo(new Point(cx + Math.Cos(a) * radius, cy + Math.Sin(a) * radius)); + } + g.EndFigure(false); + + ctx.DrawGeometry(null, pen, stream); + } + + private void DrawWaveLine(DrawingContext ctx, Rect r, double baseY, double shift, int index, Color color, double alpha) + { + var pen = new Pen(new SolidColorBrush(color, (float)alpha), Math.Max(1.5, r.Width / 100), lineCap: PenLineCap.Round); + var startX = r.Width * 0.05 + shift; + var endX = r.Width * 0.95 + shift; + + var stream = new StreamGeometry(); + var g = stream.Open(); + g.BeginFigure(new Point(startX, baseY), false); + for (var x = startX; x <= endX; x += 3) + { + var waveY = baseY + Math.Sin((x - startX) / (endX - startX) * Math.PI * 3 + _phase * Math.PI * 2 + index * 1.3) * (5 + index * 2.5); + g.LineTo(new Point(x, waveY)); + } + g.EndFigure(false); + ctx.DrawGeometry(null, pen, stream); + } + + private void DrawCloudOutline(DrawingContext ctx, double cx, double cy, double rx, double ry, Pen pen) + { + ctx.DrawEllipse(null, pen, new Point(cx, cy), rx, ry); + ctx.DrawEllipse(null, pen, new Point(cx + rx * 0.6, cy - ry * 0.3), rx * 0.7, ry * 0.7); + ctx.DrawEllipse(null, pen, new Point(cx - rx * 0.4, cy + ry * 0.2), rx * 0.5, ry * 0.5); + } + + private void DrawRain(DrawingContext ctx, Rect rect, bool storm) + { + var drops = Math.Clamp((int)(rect.Width / 22), 8, 22); + var brush = new SolidColorBrush(_palette.AccentShape, storm ? 0.72 : 0.52); + var pen = new Pen(brush, Math.Max(1.4, rect.Width / 150), lineCap: PenLineCap.Round); + for (var i = 0; i < drops; i++) + { + var t = (_phase + i * 0.137) % 1d; + var x = rect.Width * (0.18 + (i % drops) / (double)drops * 0.72); + var y = rect.Height * (0.36 + t * 0.66); + ctx.DrawLine(pen, new Point(x, y), new Point(x - rect.Width * 0.025, y + rect.Height * 0.09)); + } + + if (storm) + { + var bolt = new StreamGeometry(); + var g = bolt.Open(); + g.BeginFigure(new Point(rect.Width * 0.70, rect.Height * 0.42), true); + g.LineTo(new Point(rect.Width * 0.61, rect.Height * 0.64)); + g.LineTo(new Point(rect.Width * 0.69, rect.Height * 0.61)); + g.LineTo(new Point(rect.Width * 0.58, rect.Height * 0.86)); + g.EndFigure(true); + ctx.DrawGeometry(new SolidColorBrush(_palette.AccentShape, 0.86), null, bolt); + } + } + + private void DrawSnow(DrawingContext ctx, Rect rect) + { + var flakes = Math.Clamp((int)(rect.Width / 24), 7, 20); + var brush = new SolidColorBrush(Colors.White, 0.72); + for (var i = 0; i < flakes; i++) + { + var t = (_phase * 0.45 + i * 0.113) % 1d; + var x = rect.Width * (0.12 + (i % flakes) / (double)flakes * 0.78) + Math.Sin(t * Math.PI * 2) * 8; + var y = rect.Height * (0.20 + t * 0.82); + ctx.DrawEllipse(brush, null, new Point(x, y), 2.2, 2.2); + } + } + + private void DrawFog(DrawingContext ctx, Rect rect) + { + var pen = new Pen(new SolidColorBrush(_palette.TextSecondary, 0.28), Math.Max(2, rect.Height / 56), lineCap: PenLineCap.Round); + for (var i = 0; i < 4; i++) + { + var y = rect.Height * (0.48 + i * 0.11); + var shift = Math.Sin(_phase * Math.PI * 2 + i) * rect.Width * 0.04; + ctx.DrawLine(pen, new Point(rect.Width * 0.18 + shift, y), new Point(rect.Width * 0.82 + shift, y)); + } + } + + private IBrush CreateLinearBrush(Color top, Color bottom, double sx, double sy, double ex, double ey) + { + return new LinearGradientBrush + { + StartPoint = new RelativePoint(sx, sy, RelativeUnit.Relative), + EndPoint = new RelativePoint(ex, ey, RelativeUnit.Relative), + GradientStops = { new GradientStop(top, 0), new GradientStop(bottom, 1) } + }; + } +} diff --git a/LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs b/LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs new file mode 100644 index 0000000..50533a0 --- /dev/null +++ b/LanMountainDesktop/Views/Components/MaterialWeatherVisualTheme.cs @@ -0,0 +1,248 @@ +using System; +using Avalonia.Media; +using LanMountainDesktop.Models; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.Views.Components; + +public enum MaterialWeatherCondition +{ + Unknown, + Clear, + PartlyCloudy, + Cloudy, + Rain, + Storm, + Snow, + Fog, + Haze +} + +public sealed record MaterialWeatherPalette( + Color BackgroundTop, + Color BackgroundBottom, + Color PrimaryShape, + Color SecondaryShape, + Color AccentShape, + Color TextPrimary, + Color TextSecondary, + Color SurfaceTint, + Color OverlayTint, + Color SurfaceColor, + Color SurfaceVariantColor, + Color OutlineColor); + +public static class MaterialWeatherVisualTheme +{ + public static MaterialWeatherCondition ResolveCondition(WeatherSnapshot? snapshot) + { + return ResolveCondition(snapshot?.Current.WeatherCode, snapshot?.Current.WeatherText); + } + + public static MaterialWeatherCondition ResolveCondition(int? weatherCode, string? weatherText) + { + if (weatherCode.HasValue) + { + return weatherCode.Value switch + { + 0 => MaterialWeatherCondition.Clear, + 1 => MaterialWeatherCondition.PartlyCloudy, + 2 => MaterialWeatherCondition.Cloudy, + 3 or 7 or 8 or 9 or 10 or 11 or 12 or 19 or 21 or 22 or 23 or 24 or 25 or 301 => MaterialWeatherCondition.Rain, + 4 or 5 => MaterialWeatherCondition.Storm, + 6 or 13 or 14 or 15 or 16 or 17 or 26 or 27 or 28 or 302 => MaterialWeatherCondition.Snow, + 18 or 32 or 49 or 57 or 58 => MaterialWeatherCondition.Fog, + 20 or 29 or 30 or 31 or 53 or 54 or 55 or 56 => MaterialWeatherCondition.Haze, + _ => MaterialWeatherCondition.Unknown + }; + } + + var text = weatherText?.Trim().ToLowerInvariant() ?? string.Empty; + if (text.Contains("rain") || text.Contains('\u96e8')) return MaterialWeatherCondition.Rain; + if (text.Contains("storm") || text.Contains("thunder") || text.Contains('\u96f7')) return MaterialWeatherCondition.Storm; + if (text.Contains("snow") || text.Contains('\u96ea')) return MaterialWeatherCondition.Snow; + if (text.Contains("fog") || text.Contains("mist") || text.Contains('\u96fe')) return MaterialWeatherCondition.Fog; + if (text.Contains("haze") || text.Contains("dust") || text.Contains('\u973e')) return MaterialWeatherCondition.Haze; + if (text.Contains("cloud") || text.Contains('\u4e91') || text.Contains('\u9634')) return MaterialWeatherCondition.Cloudy; + if (text.Contains("clear") || text.Contains("sun") || text.Contains('\u6674')) return MaterialWeatherCondition.Clear; + return MaterialWeatherCondition.Unknown; + } + + public static MaterialWeatherPalette ResolvePalette(string? styleId, MaterialWeatherCondition condition, bool isNight) + { + var normalized = WeatherVisualStyleCatalog.Normalize(styleId); + return normalized switch + { + WeatherVisualStyleId.Geometric => ResolveGeometricPalette(condition, isNight), + WeatherVisualStyleId.Breezy => ResolveBreezyPalette(condition, isNight), + WeatherVisualStyleId.LemonFlutter => ResolveLemonPalette(condition, isNight), + _ => ResolveGooglePalette(condition, isNight) + }; + } + + public static MaterialWeatherPalette ResolvePalette(MaterialWeatherCondition condition, bool isNight) + { + return ResolveGooglePalette(condition, isNight); + } + + private static MaterialWeatherPalette ResolveGooglePalette(MaterialWeatherCondition condition, bool isNight) + { + if (isNight) + { + return condition switch + { + MaterialWeatherCondition.Clear => P("#0D47A1", "#1A237E", "#FFD54F", "#6EA8FE", "#B9C7FF", "#E8EAF6", "#9FA8DA", "#1A237E", "#1A000000"), + MaterialWeatherCondition.PartlyCloudy => P("#1565C0", "#283593", "#FFD54F", "#8EA2D9", "#B9C7FF", "#E8EAF6", "#9FA8DA", "#283593", "#1A000000"), + MaterialWeatherCondition.Cloudy => P("#37474F", "#455A64", "#B0BEC5", "#78909C", "#CFD8DC", "#ECEFF1", "#90A4AE", "#455A64", "#1A000000"), + MaterialWeatherCondition.Rain or MaterialWeatherCondition.Storm => P("#263238", "#37474F", "#78909C", "#546E7A", "#90A4AE", "#CFD8DC", "#90A4AE", "#37474F", "#1A000000"), + MaterialWeatherCondition.Snow => P("#1A237E", "#283593", "#E8EAF6", "#9FA8DA", "#FFFFFF", "#F5F5F5", "#B0BEC5", "#283593", "#1A000000"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#455A64", "#546E7A", "#B0BEC5", "#78909C", "#CFD8DC", "#ECEFF1", "#90A4AE", "#546E7A", "#1A000000"), + _ => P("#0D47A1", "#1A237E", "#FFD54F", "#6EA8FE", "#B9C7FF", "#E8EAF6", "#9FA8DA", "#1A237E", "#1A000000") + }; + } + + return condition switch + { + MaterialWeatherCondition.Clear => P("#4FC3F7", "#B3E5FC", "#FFD54F", "#FFF176", "#4FC3F7", "#0D47A1", "#1565C0", "#B3E5FC", "#14FFFFFF"), + MaterialWeatherCondition.PartlyCloudy => P("#81D4FA", "#E1F5FE", "#FFD54F", "#E1F5FE", "#81D4FA", "#0D47A1", "#1565C0", "#E1F5FE", "#12FFFFFF"), + MaterialWeatherCondition.Cloudy => P("#90A4AE", "#CFD8DC", "#CFD8DC", "#B0BEC5", "#78909C", "#263238", "#455A64", "#CFD8DC", "#10FFFFFF"), + MaterialWeatherCondition.Rain => P("#78909C", "#B0BEC5", "#90A4AE", "#78909C", "#546E7A", "#263238", "#37474F", "#B0BEC5", "#0EFFFFFF"), + MaterialWeatherCondition.Storm => P("#546E7A", "#78909C", "#607D8B", "#546E7A", "#FFCE5C", "#1A1A2E", "#37474F", "#78909C", "#12FFFFFF"), + MaterialWeatherCondition.Snow => P("#E1F5FE", "#FFFFFF", "#FFFFFF", "#B3E5FC", "#81D4FA", "#0D47A1", "#1565C0", "#FFFFFF", "#18FFFFFF"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#B0BEC5", "#ECEFF1", "#CFD8DC", "#B0BEC5", "#90A4AE", "#37474F", "#546E7A", "#ECEFF1", "#10FFFFFF"), + _ => P("#4FC3F7", "#B3E5FC", "#FFD54F", "#FFF176", "#4FC3F7", "#0D47A1", "#1565C0", "#B3E5FC", "#14FFFFFF") + }; + } + + private static MaterialWeatherPalette ResolveGeometricPalette(MaterialWeatherCondition condition, bool isNight) + { + if (isNight) + { + return condition switch + { + MaterialWeatherCondition.Clear => P("#0A0E27", "#1A1A3E", "#1A237E", "#283593", "#3F51B5", "#C5CAE9", "#7986CB", "#1A1A3E", "#0CFFFFFF"), + MaterialWeatherCondition.PartlyCloudy => P("#0D1033", "#1E1E4A", "#1A237E", "#303F9F", "#5C6BC0", "#C5CAE9", "#7986CB", "#1E1E4A", "#0CFFFFFF"), + MaterialWeatherCondition.Cloudy => P("#1A1A2E", "#2D2D44", "#37474F", "#455A64", "#607D8B", "#CFD8DC", "#90A4AE", "#2D2D44", "#0AFFFFFF"), + MaterialWeatherCondition.Rain or MaterialWeatherCondition.Storm => P("#0A0E27", "#1A1A3E", "#1A237E", "#303F9F", "#3F51B5", "#C5CAE9", "#7986CB", "#1A1A3E", "#0EFFFFFF"), + MaterialWeatherCondition.Snow => P("#1A237E", "#283593", "#E8EAF6", "#9FA8DA", "#C5CAE9", "#E8EAF6", "#9FA8DA", "#283593", "#0CFFFFFF"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#1A1A2E", "#37474F", "#455A64", "#546E7A", "#78909C", "#CFD8DC", "#90A4AE", "#37474F", "#0AFFFFFF"), + _ => P("#0A0E27", "#1A1A3E", "#1A237E", "#283593", "#3F51B5", "#C5CAE9", "#7986CB", "#1A1A3E", "#0CFFFFFF") + }; + } + + return condition switch + { + MaterialWeatherCondition.Clear => P("#1A237E", "#3949AB", "#5C6BC0", "#3F51B5", "#7986CB", "#E8EAF6", "#9FA8DA", "#3949AB", "#08FFFFFF"), + MaterialWeatherCondition.PartlyCloudy => P("#283593", "#5C6BC0", "#7986CB", "#5C6BC0", "#9FA8DA", "#E8EAF6", "#9FA8DA", "#5C6BC0", "#08FFFFFF"), + MaterialWeatherCondition.Cloudy => P("#37474F", "#607D8B", "#78909C", "#607D8B", "#90A4AE", "#ECEFF1", "#B0BEC5", "#607D8B", "#08FFFFFF"), + MaterialWeatherCondition.Rain => P("#1A237E", "#3F51B5", "#5C6BC0", "#3F51B5", "#7986CB", "#E8EAF6", "#9FA8DA", "#3F51B5", "#0AFFFFFF"), + MaterialWeatherCondition.Storm => P("#1A1A2E", "#3F51B5", "#5C6BC0", "#303F9F", "#FFCE5C", "#E8EAF6", "#9FA8DA", "#3F51B5", "#0CFFFFFF"), + MaterialWeatherCondition.Snow => P("#E8EAF6", "#C5CAE9", "#FFFFFF", "#9FA8DA", "#7986CB", "#1A237E", "#283593", "#C5CAE9", "#0CFFFFFF"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#455A64", "#78909C", "#90A4AE", "#78909C", "#B0BEC5", "#ECEFF1", "#B0BEC5", "#78909C", "#08FFFFFF"), + _ => P("#1A237E", "#3949AB", "#5C6BC0", "#3F51B5", "#7986CB", "#E8EAF6", "#9FA8DA", "#3949AB", "#08FFFFFF") + }; + } + + private static MaterialWeatherPalette ResolveBreezyPalette(MaterialWeatherCondition condition, bool isNight) + { + if (isNight) + { + return condition switch + { + MaterialWeatherCondition.Clear => P("#006064", "#00838F", "#4DD0E1", "#00ACC1", "#80DEEA", "#E0F7FA", "#80DEEA", "#00838F", "#0E000000"), + MaterialWeatherCondition.PartlyCloudy => P("#00695C", "#00897B", "#4DB6AC", "#009688", "#80CBC4", "#E0F2F1", "#80CBC4", "#00897B", "#0E000000"), + MaterialWeatherCondition.Cloudy => P("#37474F", "#546E7A", "#78909C", "#607D8B", "#90A4AE", "#ECEFF1", "#B0BEC5", "#546E7A", "#0E000000"), + MaterialWeatherCondition.Rain or MaterialWeatherCondition.Storm => P("#004D40", "#00695C", "#4DB6AC", "#00897B", "#80CBC4", "#E0F2F1", "#80CBC4", "#00695C", "#10000000"), + MaterialWeatherCondition.Snow => P("#006064", "#00838F", "#E0F7FA", "#80DEEA", "#FFFFFF", "#E0F7FA", "#80DEEA", "#00838F", "#0E000000"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#37474F", "#546E7A", "#78909C", "#607D8B", "#B0BEC5", "#ECEFF1", "#B0BEC5", "#546E7A", "#0E000000"), + _ => P("#006064", "#00838F", "#4DD0E1", "#00ACC1", "#80DEEA", "#E0F7FA", "#80DEEA", "#00838F", "#0E000000") + }; + } + + return condition switch + { + MaterialWeatherCondition.Clear => P("#4DD0E1", "#80DEEA", "#26C6DA", "#00BCD4", "#B2EBF2", "#004D40", "#00695C", "#80DEEA", "#12FFFFFF"), + MaterialWeatherCondition.PartlyCloudy => P("#4FC3F7", "#B2EBF2", "#29B6F6", "#03A9F4", "#E1F5FE", "#004D40", "#00695C", "#B2EBF2", "#12FFFFFF"), + MaterialWeatherCondition.Cloudy => P("#80CBC4", "#B2DFDB", "#A7D9D2", "#80CBC4", "#B2DFDB", "#004D40", "#00695C", "#B2DFDB", "#10FFFFFF"), + MaterialWeatherCondition.Rain => P("#4DB6AC", "#80CBC4", "#66BB6A", "#4DB6AC", "#A7D9D2", "#004D40", "#00695C", "#80CBC4", "#0EFFFFFF"), + MaterialWeatherCondition.Storm => P("#26A69A", "#4DB6AC", "#00897B", "#26A69A", "#FFCE5C", "#004D40", "#00695C", "#4DB6AC", "#12FFFFFF"), + MaterialWeatherCondition.Snow => P("#E0F7FA", "#FFFFFF", "#FFFFFF", "#B2EBF2", "#80DEEA", "#004D40", "#00695C", "#FFFFFF", "#16FFFFFF"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#80CBC4", "#E0F7FA", "#A7D9D2", "#80CBC4", "#B2DFDB", "#004D40", "#00695C", "#E0F7FA", "#10FFFFFF"), + _ => P("#4DD0E1", "#80DEEA", "#26C6DA", "#00BCD4", "#B2EBF2", "#004D40", "#00695C", "#80DEEA", "#12FFFFFF") + }; + } + + private static MaterialWeatherPalette ResolveLemonPalette(MaterialWeatherCondition condition, bool isNight) + { + if (isNight) + { + return condition switch + { + MaterialWeatherCondition.Clear => P("#1A237E", "#311B92", "#FFD54F", "#7C4DFF", "#B388FF", "#E8EAF6", "#B39DDB", "#311B92", "#12000000"), + MaterialWeatherCondition.PartlyCloudy => P("#283593", "#4A148C", "#FFD54F", "#7C4DFF", "#B388FF", "#EDE7F6", "#B39DDB", "#4A148C", "#12000000"), + MaterialWeatherCondition.Cloudy => P("#37474F", "#4E342E", "#8D6E63", "#6D4C41", "#BCAAA4", "#EFEBE9", "#BCAAA4", "#4E342E", "#12000000"), + MaterialWeatherCondition.Rain or MaterialWeatherCondition.Storm => P("#1A1A2E", "#311B92", "#7C4DFF", "#5C6BC0", "#9FA8DA", "#D1C4E9", "#9575CD", "#311B92", "#14000000"), + MaterialWeatherCondition.Snow => P("#1A237E", "#311B92", "#E8EAF6", "#B39DDB", "#FFFFFF", "#F3E5F5", "#CE93D8", "#311B92", "#12000000"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#4E342E", "#5D4037", "#8D6E63", "#6D4C41", "#BCAAA4", "#EFEBE9", "#BCAAA4", "#5D4037", "#12000000"), + _ => P("#1A237E", "#311B92", "#FFD54F", "#7C4DFF", "#B388FF", "#E8EAF6", "#B39DDB", "#311B92", "#12000000") + }; + } + + return condition switch + { + MaterialWeatherCondition.Clear => P("#FFB74D", "#FFF176", "#FF9800", "#FFC107", "#FFE082", "#4E342E", "#6D4C41", "#FFF176", "#0EFFFFFF"), + MaterialWeatherCondition.PartlyCloudy => P("#FF8A65", "#FFCC80", "#FF7043", "#FFA726", "#FFE0B2", "#4E342E", "#6D4C41", "#FFCC80", "#0EFFFFFF"), + MaterialWeatherCondition.Cloudy => P("#BCAAA4", "#D7CCC8", "#A1887F", "#8D6E63", "#BCAAA4", "#3E2723", "#5D4037", "#D7CCC8", "#0EFFFFFF"), + MaterialWeatherCondition.Rain => P("#90A4AE", "#B0BEC5", "#78909C", "#607D8B", "#B0BEC5", "#263238", "#37474F", "#B0BEC5", "#0CFFFFFF"), + MaterialWeatherCondition.Storm => P("#78909C", "#90A4AE", "#607D8B", "#546E7A", "#FFCE5C", "#1A1A2E", "#37474F", "#90A4AE", "#10FFFFFF"), + MaterialWeatherCondition.Snow => P("#FFF9C4", "#FFFFFF", "#FFFFFF", "#FFF9C4", "#FFF176", "#4E342E", "#6D4C41", "#FFFFFF", "#12FFFFFF"), + MaterialWeatherCondition.Fog or MaterialWeatherCondition.Haze => P("#D7CCC8", "#EFEBE9", "#BCAAA4", "#A1887F", "#D7CCC8", "#3E2723", "#5D4037", "#EFEBE9", "#0CFFFFFF"), + _ => P("#FFB74D", "#FFF176", "#FF9800", "#FFC107", "#FFE082", "#4E342E", "#6D4C41", "#FFF176", "#0EFFFFFF") + }; + } + + public static string ResolveDisplayText(WeatherSnapshot? snapshot, string fallback) + { + var text = snapshot?.Current.WeatherText?.Trim(); + if (!string.IsNullOrWhiteSpace(text)) + { + return text!; + } + + return ResolveCondition(snapshot) switch + { + MaterialWeatherCondition.Clear => "Clear", + MaterialWeatherCondition.PartlyCloudy => "Partly cloudy", + MaterialWeatherCondition.Cloudy => "Cloudy", + MaterialWeatherCondition.Rain => "Rain", + MaterialWeatherCondition.Storm => "Storm", + MaterialWeatherCondition.Snow => "Snow", + MaterialWeatherCondition.Fog => "Fog", + MaterialWeatherCondition.Haze => "Haze", + _ => fallback + }; + } + + private static MaterialWeatherPalette P(string bgTop, string bgBottom, string primary, string secondary, string accent, string textPrimary, string textSecondary, string surfaceTint, string overlayTint) + { + var isDark = IsDarkBackground(C(bgTop), C(bgBottom)); + var surfaceColor = isDark ? Color.Parse("#1AFFFFFF") : Color.Parse("#14000000"); + var surfaceVariantColor = isDark ? Color.Parse("#12FFFFFF") : Color.Parse("#0E000000"); + var outlineColor = isDark ? Color.Parse("#24FFFFFF") : Color.Parse("#1C000000"); + return new MaterialWeatherPalette( + C(bgTop), C(bgBottom), C(primary), C(secondary), C(accent), + C(textPrimary), C(textSecondary), C(surfaceTint), C(overlayTint), + surfaceColor, surfaceVariantColor, outlineColor); + } + + private static bool IsDarkBackground(Color top, Color bottom) + { + var luminance = (0.299 * top.R + 0.587 * top.G + 0.114 * top.B) / 255; + return luminance < 0.5; + } + + private static Color C(string value) + { + return Color.Parse(value); + } +} diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml new file mode 100644 index 0000000..0339bc5 --- /dev/null +++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs new file mode 100644 index 0000000..0a430cf --- /dev/null +++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs @@ -0,0 +1,88 @@ +using System.Linq; +using Avalonia.Controls; +using LanMountainDesktop.Models; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.Views.Components; + +public partial class MultiDayWeatherWidget : WeatherWidgetBase +{ + public MultiDayWeatherWidget() + { + InitializeComponent(); + RenderWeather(); + } + + protected override MaterialWeatherSceneControl SceneControl => Scene; + + protected override void ApplyResponsiveLayout(double cellSize) + { + var scale = cellSize / 64d; + ContentGrid.Margin = new Avalonia.Thickness(16 * scale, 12 * scale); + ContentGrid.ColumnSpacing = 12 * scale; + TemperatureTextBlock.FontSize = System.Math.Clamp(40 * scale, 26, 58); + ConditionTextBlock.FontSize = System.Math.Clamp(14 * scale, 10, 20); + LocationTextBlock.FontSize = System.Math.Clamp(11 * scale, 8, 16); + MainIcon.Width = System.Math.Clamp(56 * scale, 36, 80); + MainIcon.Height = System.Math.Clamp(56 * scale, 36, 80); + } + + protected override void RenderWeather() + { + RootBorder.Background = Brush(CurrentPalette.BackgroundBottom); + OverlayBorder.Background = new Avalonia.Media.SolidColorBrush(CurrentPalette.OverlayTint); + TemperatureTextBlock.Foreground = Brush(CurrentPalette.TextPrimary); + ConditionTextBlock.Foreground = Brush(CurrentPalette.TextPrimary, 0.88); + LocationTextBlock.Foreground = Brush(CurrentPalette.TextSecondary, 0.78); + TemperatureTextBlock.Text = FormatTemperature(Snapshot?.Current.TemperatureC); + MainIcon.SetWeatherIcon(CurrentVisualStyleId, Snapshot); + ConditionTextBlock.Text = State == WeatherWidgetState.Error ? "Weather unavailable" : MaterialWeatherVisualTheme.ResolveDisplayText(Snapshot, StatusText); + LocationTextBlock.Text = DisplayLocation; + BuildDailyItems(); + } + + private void BuildDailyItems() + { + var forecasts = Snapshot?.DailyForecasts.Take(5).ToArray() ?? CreatePreviewSnapshot().DailyForecasts.Take(5).ToArray(); + var panel = new StackPanel { Spacing = 0, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; + for (var i = 0; i < forecasts.Length; i++) + { + panel.Children.Add(CreateRow(forecasts[i], i < forecasts.Length - 1)); + } + + DailyItemsControl.ItemsSource = new[] { panel }; + } + + private Control CreateRow(WeatherDailyForecast item, bool addDivider) + { + var rowPanel = new StackPanel { Spacing = 0 }; + + var row = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto,Auto"), + ColumnSpacing = 10, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + row.Children.Add(new WeatherIconView { Width = 24, Height = 24, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.DayWeatherCode, item.DayWeatherText), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }); + row.Children.Add(new TextBlock { Text = ResolveDayLabel(item.Date), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextTrimming = Avalonia.Media.TextTrimming.CharacterEllipsis, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12 }); + Grid.SetColumn(row.Children[^1], 1); + row.Children.Add(new TextBlock { Text = FormatTemperature(item.HighTemperatureC), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12 }); + Grid.SetColumn(row.Children[^1], 2); + row.Children.Add(new TextBlock { Text = FormatTemperature(item.LowTemperatureC), Foreground = Brush(CurrentPalette.TextSecondary), FontWeight = Avalonia.Media.FontWeight.Medium, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12 }); + Grid.SetColumn(row.Children[^1], 3); + + rowPanel.Children.Add(row); + + if (addDivider) + { + rowPanel.Children.Add(new Border + { + Height = 1, + Background = new Avalonia.Media.SolidColorBrush(CurrentPalette.OutlineColor), + Margin = new Avalonia.Thickness(0, 6, 0, 6) + }); + } + + return rowPanel; + } +} diff --git a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml new file mode 100644 index 0000000..54b2be3 --- /dev/null +++ b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs new file mode 100644 index 0000000..3e47132 --- /dev/null +++ b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs @@ -0,0 +1,88 @@ +using System; +using Avalonia.Threading; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.Views.Components; + +public partial class WeatherClockWidget : WeatherWidgetBase, ITimeZoneAwareComponentWidget +{ + private readonly DispatcherTimer _clockTimer = new() { Interval = TimeSpan.FromSeconds(1) }; + private TimeZoneService? _timeZoneService; + + public WeatherClockWidget() + { + InitializeComponent(); + _clockTimer.Tick += (_, _) => UpdateClock(); + AttachedToVisualTree += (_, _) => + { + UpdateClock(); + _clockTimer.Start(); + }; + DetachedFromVisualTree += (_, _) => _clockTimer.Stop(); + RenderWeather(); + } + + protected override MaterialWeatherSceneControl SceneControl => Scene; + + public void SetTimeZoneService(TimeZoneService timeZoneService) + { + ClearTimeZoneService(); + _timeZoneService = timeZoneService; + _timeZoneService.TimeZoneChanged += OnTimeZoneChanged; + UpdateClock(); + } + + public void ClearTimeZoneService() + { + if (_timeZoneService is null) + { + return; + } + + _timeZoneService.TimeZoneChanged -= OnTimeZoneChanged; + _timeZoneService = null; + } + + protected override void ApplyResponsiveLayout(double cellSize) + { + var scale = cellSize / 64d; + ContentGrid.Margin = new Avalonia.Thickness(16 * scale, 10 * scale); + ContentGrid.ColumnSpacing = 10 * scale; + TimeTextBlock.FontSize = System.Math.Clamp(36 * scale, 22, 52); + DateTextBlock.FontSize = System.Math.Clamp(12 * scale, 8, 16); + TemperatureTextBlock.FontSize = System.Math.Clamp(20 * scale, 14, 30); + ConditionTextBlock.FontSize = System.Math.Clamp(11 * scale, 8, 14); + MainIcon.Width = System.Math.Clamp(40 * scale, 24, 56); + MainIcon.Height = System.Math.Clamp(40 * scale, 24, 56); + } + + protected override void RenderWeather() + { + RootBorder.Background = Brush(CurrentPalette.BackgroundBottom); + OverlayBorder.Background = new Avalonia.Media.SolidColorBrush(CurrentPalette.OverlayTint); + TimeTextBlock.Foreground = Brush(CurrentPalette.TextPrimary); + DateTextBlock.Foreground = Brush(CurrentPalette.TextSecondary); + TemperatureTextBlock.Foreground = Brush(CurrentPalette.TextPrimary); + ConditionTextBlock.Foreground = Brush(CurrentPalette.TextSecondary); + TemperatureTextBlock.Text = FormatTemperature(Snapshot?.Current.TemperatureC); + MainIcon.SetWeatherIcon(CurrentVisualStyleId, Snapshot); + ConditionTextBlock.Text = State == WeatherWidgetState.MissingLocation + ? "Set location" + : State == WeatherWidgetState.Error + ? "Unavailable" + : MaterialWeatherVisualTheme.ResolveDisplayText(Snapshot, "Weather"); + UpdateClock(); + } + + private void UpdateClock() + { + var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now; + TimeTextBlock.Text = now.ToString("HH:mm"); + DateTextBlock.Text = $"{now:ddd, MMM d} · {DisplayLocation}"; + } + + private void OnTimeZoneChanged(object? sender, EventArgs e) + { + UpdateClock(); + } +} diff --git a/LanMountainDesktop/Views/Components/WeatherIconView.cs b/LanMountainDesktop/Views/Components/WeatherIconView.cs new file mode 100644 index 0000000..0027081 --- /dev/null +++ b/LanMountainDesktop/Views/Components/WeatherIconView.cs @@ -0,0 +1,30 @@ +using Avalonia.Controls; +using Avalonia.Media; +using LanMountainDesktop.Models; +using LanMountainDesktop.Services; + +namespace LanMountainDesktop.Views.Components; + +public sealed class WeatherIconView : Image +{ + public WeatherIconView() + { + Stretch = Stretch.Uniform; + IsHitTestVisible = false; + } + + public void SetWeatherIcon(string? styleId, string iconKey) + { + Source = WeatherIconAssetResolver.LoadIcon(styleId, iconKey); + } + + public void SetWeatherIcon(string? styleId, WeatherSnapshot? snapshot) + { + Source = WeatherIconAssetResolver.LoadIcon(styleId, snapshot); + } + + public void SetWeatherIcon(string? styleId, int? weatherCode, string? weatherText, bool isDaylight = true) + { + Source = WeatherIconAssetResolver.LoadIcon(styleId, weatherCode, weatherText, isDaylight); + } +} diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml b/LanMountainDesktop/Views/Components/WeatherWidget.axaml new file mode 100644 index 0000000..6255dcf --- /dev/null +++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs new file mode 100644 index 0000000..46fda0e --- /dev/null +++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs @@ -0,0 +1,48 @@ +using Avalonia.Media; + +namespace LanMountainDesktop.Views.Components; + +public partial class WeatherWidget : WeatherWidgetBase +{ + public WeatherWidget() + { + InitializeComponent(); + RenderWeather(); + } + + protected override MaterialWeatherSceneControl SceneControl => Scene; + + protected override void ApplyResponsiveLayout(double cellSize) + { + var scale = cellSize / 64d; + ContentGrid.Margin = new Avalonia.Thickness(18 * scale, 14 * scale, 18 * scale, 12 * scale); + TemperatureTextBlock.FontSize = System.Math.Clamp(68 * scale, 40, 92); + ConditionTextBlock.FontSize = System.Math.Clamp(16 * scale, 12, 24); + LocationTextBlock.FontSize = System.Math.Clamp(12 * scale, 9, 16); + RangeTextBlock.FontSize = System.Math.Clamp(12 * scale, 9, 16); + MainIcon.Width = System.Math.Clamp(64 * scale, 40, 88); + MainIcon.Height = System.Math.Clamp(64 * scale, 40, 88); + } + + protected override void RenderWeather() + { + RootBorder.Background = Brush(CurrentPalette.BackgroundBottom); + OverlayBorder.Background = new SolidColorBrush(CurrentPalette.OverlayTint); + TemperatureTextBlock.Foreground = Brush(CurrentPalette.TextPrimary); + ConditionTextBlock.Foreground = Brush(CurrentPalette.TextPrimary, 0.88); + LocationTextBlock.Foreground = Brush(CurrentPalette.TextSecondary, 0.78); + RangeTextBlock.Foreground = Brush(CurrentPalette.TextSecondary); + + TemperatureTextBlock.Text = State == WeatherWidgetState.MissingLocation ? "--\u00b0" : FormatTemperature(Snapshot?.Current.TemperatureC); + ConditionTextBlock.Text = State switch + { + WeatherWidgetState.MissingLocation => "Set weather location", + WeatherWidgetState.Error => "Weather unavailable", + WeatherWidgetState.Loading => "Loading", + _ => MaterialWeatherVisualTheme.ResolveDisplayText(Snapshot, "Weather") + }; + LocationTextBlock.Text = DisplayLocation; + RangeTextBlock.Text = FormatRange(Snapshot); + MainIcon.SetWeatherIcon(CurrentVisualStyleId, Snapshot); + } +} diff --git a/LanMountainDesktop/Views/Components/WeatherWidgetBase.cs b/LanMountainDesktop/Views/Components/WeatherWidgetBase.cs new file mode 100644 index 0000000..9b641cb --- /dev/null +++ b/LanMountainDesktop/Views/Components/WeatherWidgetBase.cs @@ -0,0 +1,423 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Threading; +using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.Host.Abstractions; +using LanMountainDesktop.Models; +using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; + +namespace LanMountainDesktop.Views.Components; + +public enum WeatherWidgetState +{ + Loading, + Ready, + MissingLocation, + Error, + Preview +} + +public abstract class WeatherWidgetBase : UserControl, + IDesktopComponentWidget, + IDesktopPageVisibilityAwareComponentWidget, + IWeatherInfoAwareComponentWidget, + IComponentRuntimeContextAware, + IComponentPlacementContextAware, + IComponentChromeContextAware +{ + private readonly DispatcherTimer _refreshTimer = new(); + private CancellationTokenSource? _refreshCancellation; + private bool _isAttached; + private bool _isOnActivePage = true; + private bool _isEditMode; + private bool _hasStarted; + private bool _isSettingsSubscribed; + private DesktopComponentRenderMode _renderMode = DesktopComponentRenderMode.Live; + + private ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); + private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; + private IWeatherInfoService _weatherInfoService = CreateDefaultWeatherInfoService(); + private string _componentId = string.Empty; + private string? _placementId; + private double _cellSize = 64; + + protected WeatherWidgetBase() + { + _refreshTimer.Tick += (_, _) => _ = RefreshWeatherAsync(forceRefresh: true); + AttachedToVisualTree += OnAttachedToVisualTree; + DetachedFromVisualTree += OnDetachedFromVisualTree; + SizeChanged += (_, _) => ApplyCellSize(_cellSize); + } + + protected WeatherWidgetState State { get; private set; } = WeatherWidgetState.Loading; + + protected WeatherSnapshot? Snapshot { get; private set; } + + protected string DisplayLocation { get; private set; } = "Weather"; + + protected string StatusText { get; private set; } = "Loading"; + + protected MaterialWeatherCondition CurrentCondition { get; private set; } = MaterialWeatherCondition.Clear; + + protected MaterialWeatherPalette CurrentPalette { get; private set; } = MaterialWeatherVisualTheme.ResolvePalette(MaterialWeatherCondition.Clear, false); + + protected string CurrentVisualStyleId { get; private set; } = WeatherVisualStyleId.Default; + + protected bool IsLiveRenderMode => _renderMode == DesktopComponentRenderMode.Live; + + protected double CurrentCellSize => _cellSize; + + protected abstract MaterialWeatherSceneControl SceneControl { get; } + + public virtual void ApplyCellSize(double cellSize) + { + _cellSize = Math.Max(24, cellSize); + ApplyResponsiveLayout(_cellSize); + } + + public void SetWeatherInfoService(IWeatherInfoService weatherInfoService) + { + _weatherInfoService = weatherInfoService ?? CreateDefaultWeatherInfoService(); + StartIfReady(); + } + + public void SetComponentRuntimeContext(DesktopComponentRuntimeContext context) + { + UnsubscribeSettings(); + _settingsFacade = context.SettingsFacade; + _settingsService = context.SettingsService; + _componentId = context.ComponentId; + _placementId = context.PlacementId; + _renderMode = context.RenderMode; + SubscribeSettings(); + StartIfReady(); + } + + public void SetComponentPlacementContext(string componentId, string? placementId) + { + _componentId = string.IsNullOrWhiteSpace(componentId) ? _componentId : componentId.Trim(); + _placementId = placementId; + ConfigureRefreshTimer(); + } + + public virtual void SetComponentChromeContext(ComponentChromeContext context) + { + ApplyCellSize(context.CellSize); + } + + public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) + { + _isOnActivePage = isOnActivePage; + _isEditMode = isEditMode; + UpdateAnimationState(); + if (_isOnActivePage) + { + StartIfReady(); + } + } + + protected abstract void ApplyResponsiveLayout(double cellSize); + + protected abstract void RenderWeather(); + + private static IWeatherInfoService CreateDefaultWeatherInfoService() + { + return HostSettingsFacadeProvider.GetOrCreate().Weather.GetWeatherInfoService(); + } + + protected virtual WeatherSnapshot CreatePreviewSnapshot() + { + var now = DateTimeOffset.Now; + return new WeatherSnapshot( + Provider: "Preview", + LocationKey: "preview", + LocationName: "Material City", + Latitude: 0, + Longitude: 0, + FetchedAt: now, + ObservationTime: now, + Current: new WeatherCurrentCondition(24, 25, 58, 42, 12, 180, 1, true, "Partly cloudy"), + DailyForecasts: + [ + new WeatherDailyForecast(DateOnly.FromDateTime(DateTime.Today), 20, 28, 1, "Partly cloudy", 2, "Cloudy", "06:10", "18:40", 20), + new WeatherDailyForecast(DateOnly.FromDateTime(DateTime.Today.AddDays(1)), 19, 26, 7, "Light rain", 7, "Light rain", "06:11", "18:40", 60), + new WeatherDailyForecast(DateOnly.FromDateTime(DateTime.Today.AddDays(2)), 18, 25, 2, "Cloudy", 2, "Cloudy", "06:12", "18:39", 30), + new WeatherDailyForecast(DateOnly.FromDateTime(DateTime.Today.AddDays(3)), 21, 30, 0, "Clear", 1, "Partly cloudy", "06:12", "18:38", 10), + new WeatherDailyForecast(DateOnly.FromDateTime(DateTime.Today.AddDays(4)), 20, 29, 1, "Partly cloudy", 1, "Partly cloudy", "06:13", "18:37", 10) + ], + HourlyForecasts: Enumerable.Range(0, 8) + .Select(i => new WeatherHourlyForecast(now.AddHours(i), 24 + (i % 4), i == 2 ? 7 : 1, i == 2 ? "Rain" : "Cloudy")) + .ToArray()); + } + + protected string FormatTemperature(double? value) + { + return value.HasValue + ? string.Format(CultureInfo.InvariantCulture, "{0:0.#}\u00b0", value.Value) + : "--\u00b0"; + } + + protected string FormatTemperatureWithUnit(double? value) + { + return value.HasValue + ? string.Format(CultureInfo.InvariantCulture, "{0:0.#}\u00b0C", value.Value) + : "--"; + } + + protected string FormatRange(WeatherSnapshot? snapshot) + { + var daily = snapshot?.DailyForecasts.FirstOrDefault(); + return daily is null + ? "-- / --" + : $"{FormatTemperature(daily.LowTemperatureC)} / {FormatTemperature(daily.HighTemperatureC)}"; + } + + protected string FormatTime(DateTimeOffset time) + { + return time.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture); + } + + protected string ResolveDayLabel(DateOnly date) + { + var today = DateOnly.FromDateTime(DateTime.Today); + if (date == today) return "Today"; + if (date == today.AddDays(1)) return "Tomorrow"; + return date.ToString("ddd", CultureInfo.InvariantCulture); + } + + protected IBrush Brush(Color color, double opacity = 1) + { + return new SolidColorBrush(color, opacity); + } + + protected void ApplyCurrentScene() + { + SceneControl.Apply(CurrentVisualStyleId, CurrentCondition, CurrentPalette, IsLiveRenderMode && _isAttached && _isOnActivePage && !_isEditMode); + } + + protected string ResolveIconKey(int? weatherCode, string? weatherText, bool isDaylight = true) + { + return WeatherIconAssetResolver.ResolveIconKey(weatherCode, weatherText, isDaylight); + } + + private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + _isAttached = true; + SubscribeSettings(); + StartIfReady(); + } + + private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + _isAttached = false; + _refreshTimer.Stop(); + _refreshCancellation?.Cancel(); + UnsubscribeSettings(); + UpdateAnimationState(); + } + + private void StartIfReady() + { + if (!_isAttached || _hasStarted) + { + UpdateAnimationState(); + return; + } + + _hasStarted = true; + ConfigureRefreshTimer(); + + if (!IsLiveRenderMode) + { + ApplySnapshot(CreatePreviewSnapshot(), WeatherWidgetState.Preview, "Material City", "Preview"); + return; + } + + _ = RefreshWeatherAsync(forceRefresh: false); + } + + private void ConfigureRefreshTimer() + { + var (enabled, interval) = ResolveRefreshSettings(); + _refreshTimer.Interval = TimeSpan.FromMinutes(interval); + if (_isAttached && IsLiveRenderMode && enabled) + { + _refreshTimer.Start(); + } + else + { + _refreshTimer.Stop(); + } + } + + private async Task RefreshWeatherAsync(bool forceRefresh) + { + if (!_isAttached || !IsLiveRenderMode || !_isOnActivePage) + { + return; + } + + var config = ResolveWeatherConfig(); + if (string.IsNullOrWhiteSpace(config.LocationKey)) + { + ApplySnapshot(null, WeatherWidgetState.MissingLocation, "Weather", "Set location in Settings"); + return; + } + + ApplySnapshot(Snapshot, Snapshot is null ? WeatherWidgetState.Loading : WeatherWidgetState.Ready, config.LocationName, "Loading"); + + _refreshCancellation?.Cancel(); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(12)); + _refreshCancellation = cts; + + try + { + var language = _settingsFacade.Region.Get().LanguageCode; + var result = await _weatherInfoService.GetWeatherAsync( + new WeatherQuery( + config.LocationKey, + config.Latitude, + config.Longitude, + ForecastDays: 7, + Locale: NormalizeWeatherLocale(language), + ForceRefresh: forceRefresh), + cts.Token); + + if (result.Success && result.Data is not null) + { + ApplySnapshot(result.Data, WeatherWidgetState.Ready, ResolveLocationName(result.Data, config.LocationName), "Ready"); + return; + } + + ApplySnapshot(Snapshot, WeatherWidgetState.Error, config.LocationName, "Weather unavailable"); + } + catch (OperationCanceledException) + { + } + catch + { + ApplySnapshot(Snapshot, WeatherWidgetState.Error, config.LocationName, "Weather unavailable"); + } + } + + private void ApplySnapshot(WeatherSnapshot? snapshot, WeatherWidgetState state, string location, string status) + { + Snapshot = snapshot; + State = state; + DisplayLocation = string.IsNullOrWhiteSpace(location) ? "Weather" : location.Trim(); + StatusText = status; + + var isNight = snapshot?.Current.IsDaylight.HasValue == true + ? !snapshot.Current.IsDaylight.Value + : _settingsFacade.Theme.Get().IsNightMode; + CurrentVisualStyleId = WeatherVisualStyleCatalog.Normalize(_settingsFacade.Weather.Get().IconPackId); + CurrentCondition = MaterialWeatherVisualTheme.ResolveCondition(snapshot); + CurrentPalette = MaterialWeatherVisualTheme.ResolvePalette(CurrentVisualStyleId, CurrentCondition, isNight); + ApplyCurrentScene(); + RenderWeather(); + } + + private void SubscribeSettings() + { + if (!_isAttached || _isSettingsSubscribed) + { + return; + } + + _settingsService.Changed += OnSettingsChanged; + _isSettingsSubscribed = true; + } + + private void UnsubscribeSettings() + { + if (!_isSettingsSubscribed) + { + return; + } + + _settingsService.Changed -= OnSettingsChanged; + _isSettingsSubscribed = false; + } + + private void OnSettingsChanged(object? sender, SettingsChangedEvent e) + { + if (e.Scope != SettingsScope.App) + { + return; + } + + if (e.ChangedKeys.Count > 0 && + !e.ChangedKeys.Contains(nameof(AppSettingsSnapshot.WeatherIconPackId))) + { + return; + } + + CurrentVisualStyleId = WeatherVisualStyleCatalog.Normalize(_settingsFacade.Weather.Get().IconPackId); + RenderWeather(); + } + + private (bool Enabled, int IntervalMinutes) ResolveRefreshSettings() + { + if (string.IsNullOrWhiteSpace(_componentId)) + { + return (true, 12); + } + + var snapshot = _settingsService.LoadSnapshot( + SettingsScope.ComponentInstance, + _componentId, + _placementId); + + return ( + snapshot.WeatherAutoRefreshEnabled, + Math.Clamp(snapshot.WeatherAutoRefreshIntervalMinutes, 5, 360)); + } + + private WeatherConfig ResolveWeatherConfig() + { + var app = _settingsFacade.Weather.Get(); + var locationKey = app.LocationKey?.Trim() ?? string.Empty; + var locationName = app.LocationName?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(locationKey) && + string.Equals(app.LocationMode, "Coordinates", StringComparison.OrdinalIgnoreCase)) + { + locationKey = FormattableString.Invariant($"coord:{app.Latitude:F4},{app.Longitude:F4}"); + } + + if (string.IsNullOrWhiteSpace(locationName)) + { + locationName = string.IsNullOrWhiteSpace(locationKey) ? "Weather" : locationKey; + } + + return new WeatherConfig(locationKey, locationName, app.Latitude, app.Longitude); + } + + private static string ResolveLocationName(WeatherSnapshot snapshot, string fallback) + { + return string.IsNullOrWhiteSpace(snapshot.LocationName) + ? fallback + : snapshot.LocationName!; + } + + private void UpdateAnimationState() + { + ApplyCurrentScene(); + } + + private static string NormalizeWeatherLocale(string? languageCode) + { + return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase) + ? "en_us" + : "zh_cn"; + } + + private readonly record struct WeatherConfig(string LocationKey, string LocationName, double Latitude, double Longitude); +} diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index 3df48b1..f1575ab 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -553,6 +553,11 @@ public partial class MainWindow : Window _taskbarLayoutMode = string.IsNullOrWhiteSpace(snapshot.TaskbarLayoutMode) ? TaskbarLayoutBottomFullRowMacStyle : snapshot.TaskbarLayoutMode; + _backToWindowsButtonDisplayMode = NormalizeBackToWindowsButtonDisplayMode(snapshot.BackToWindowsButtonDisplayMode); + _backToWindowsIconSource = NormalizeBackToWindowsIconSource(snapshot.BackToWindowsIconSource); + _backToWindowsFluentIconName = NormalizeBackToWindowsFluentIcon(snapshot.BackToWindowsFluentIconName).ToString(); + _backToWindowsIconText = NormalizeBackToWindowsIconText(snapshot.BackToWindowsIconText); + RefreshBackToWindowsButtonPresentation(); _clockDisplayFormat = snapshot.ClockDisplayFormat == "HourMinute" ? ClockDisplayFormat.HourMinute diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs index adf5344..c322e5c 100644 --- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs +++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs @@ -64,6 +64,23 @@ public partial class MainWindow : Window if (e.Scope == SettingsScope.App && e.ChangedKeys is { Count: > 0 }) { var changedKeys = e.ChangedKeys.ToArray(); + if (changedKeys.Any(key => + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsButtonDisplayMode), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsIconSource), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsFluentIconName), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsIconText), StringComparison.OrdinalIgnoreCase))) + { + Dispatcher.UIThread.Post(() => + { + var snapshot = _settingsService.LoadSnapshot(SettingsScope.App); + _backToWindowsButtonDisplayMode = NormalizeBackToWindowsButtonDisplayMode(snapshot.BackToWindowsButtonDisplayMode); + _backToWindowsIconSource = NormalizeBackToWindowsIconSource(snapshot.BackToWindowsIconSource); + _backToWindowsFluentIconName = NormalizeBackToWindowsFluentIcon(snapshot.BackToWindowsFluentIconName).ToString(); + _backToWindowsIconText = NormalizeBackToWindowsIconText(snapshot.BackToWindowsIconText); + RefreshBackToWindowsButtonPresentation(); + }, DispatcherPriority.Normal); + } + if (changedKeys.All(key => string.Equals(key, nameof(AppSettingsSnapshot.ThemeColorMode), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.SystemMaterialMode), StringComparison.OrdinalIgnoreCase) || @@ -83,6 +100,10 @@ public partial class MainWindow : Window string.Equals(key, nameof(AppSettingsSnapshot.EnableFadeTransition), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.ShowInTaskbar), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.MultiInstanceLaunchBehavior), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsButtonDisplayMode), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsIconSource), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsFluentIconName), StringComparison.OrdinalIgnoreCase) || + string.Equals(key, nameof(AppSettingsSnapshot.BackToWindowsIconText), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.EnableSlideTransition), StringComparison.OrdinalIgnoreCase))) { return; @@ -142,9 +163,11 @@ public partial class MainWindow : Window private void ApplyLocalization() { Title = L("app.title", "LanMountainDesktop"); - var platformName = OperatingSystem.IsWindows() ? "Windows" - : OperatingSystem.IsMacOS() ? "macOS" - : "Linux"; + var platformName = OperatingSystem.IsWindows() + ? L("platform.windows", "Windows") + : OperatingSystem.IsMacOS() + ? L("platform.macos", "macOS") + : L("platform.linux", "Linux"); BackToWindowsTextBlock.Text = Lf("button.back_to_platform", "Back to {0}", platformName); ToolTip.SetTip(BackToWindowsButton, Lf("tooltip.back_to_platform", "Back to {0}", platformName)); ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Widgets"); @@ -186,8 +209,7 @@ public partial class MainWindow : Window private static string NormalizeWeatherIconPackId(string? iconPackId) { - _ = iconPackId; - return "DefaultWeather"; + return WeatherVisualStyleCatalog.Normalize(iconPackId); } private void InitializeAutoStartWithWindowsSetting(AppSettingsSnapshot snapshot) @@ -668,6 +690,10 @@ public partial class MainWindow : Window PinnedTaskbarActions = [.. _pinnedTaskbarActions.Select(v => v.ToString())], EnableDynamicTaskbarActions = _enableDynamicTaskbarActions, TaskbarLayoutMode = _taskbarLayoutMode, + BackToWindowsButtonDisplayMode = existingSnapshot.BackToWindowsButtonDisplayMode, + BackToWindowsIconSource = existingSnapshot.BackToWindowsIconSource, + BackToWindowsFluentIconName = existingSnapshot.BackToWindowsFluentIconName, + BackToWindowsIconText = existingSnapshot.BackToWindowsIconText, ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond", StatusBarClockTransparentBackground = _statusBarClockTransparentBackground, ClockPosition = _clockPosition, diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml index 165f19a..a30700c 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml +++ b/LanMountainDesktop/Views/MainWindow.axaml @@ -334,14 +334,19 @@ BorderThickness="0" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Click="OnMinimizeClick"> - - + + + diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index 257d54e..43d0549 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -18,6 +19,7 @@ using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; +using FluentIcons.Avalonia; using FluentAvalonia.Styling; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; @@ -27,6 +29,8 @@ using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Shared.Contracts.Launcher; using LanMountainDesktop.Theme; using LanMountainDesktop.Views.Components; +using FluentIconKind = FluentIcons.Common.Icon; +using FluentIconVariant = FluentIcons.Common.IconVariant; namespace LanMountainDesktop.Views; @@ -62,6 +66,13 @@ public partial class MainWindow : Window private static readonly int SettingsTransitionDurationMs = (int)FluttermotionToken.Page.TotalMilliseconds; private const double LightBackgroundLuminanceThreshold = 0.57; private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle"; + private const string BackToWindowsButtonDisplayModeIconAndText = "IconAndText"; + private const string BackToWindowsButtonDisplayModeIconOnly = "IconOnly"; + private const string BackToWindowsButtonDisplayModeTextOnly = "TextOnly"; + private const string BackToWindowsIconSourceFluentIcon = "FluentIcon"; + private const string BackToWindowsIconSourceText = "Text"; + private const string DefaultBackToWindowsFluentIconName = "Circle"; + private const string DefaultBackToWindowsIconText = "○"; private static readonly HashSet SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp" @@ -159,6 +170,10 @@ public partial class MainWindow : Window private double _statusBarShadowOpacity = 0.3; private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent; private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle; + private string _backToWindowsButtonDisplayMode = BackToWindowsButtonDisplayModeIconAndText; + private string _backToWindowsIconSource = BackToWindowsIconSourceFluentIcon; + private string _backToWindowsFluentIconName = DefaultBackToWindowsFluentIconName; + private string _backToWindowsIconText = DefaultBackToWindowsIconText; private string _languageCode = "zh-CN"; private WeatherLocationMode _weatherLocationMode = WeatherLocationMode.CitySearch; private string _weatherLocationKey = string.Empty; @@ -167,7 +182,7 @@ public partial class MainWindow : Window private double _weatherLongitude = 116.4074; private bool _weatherAutoRefreshLocation; private string _weatherExcludedAlertsRaw = string.Empty; - private string _weatherIconPackId = "DefaultWeather"; + private string _weatherIconPackId = WeatherVisualStyleId.Default; private bool _weatherNoTlsRequests; private bool _autoStartWithWindows; private bool _suppressAutoStartToggleEvents; @@ -746,16 +761,18 @@ public partial class MainWindow : Window NetworkSpeedWidgetRight.Margin = new Thickness(0); NetworkSpeedWidgetRight.ApplyCellSize(cellSize); - var buttonMinWidth = Math.Clamp(taskbarCellHeight * 2.35, 100, 340); + var buttonMinWidth = GetBackToWindowsButtonMinWidth(taskbarCellHeight); BackToWindowsButton.Margin = new Thickness(0); BackToWindowsButton.Padding = taskbarButtonPadding; BackToWindowsButton.FontSize = taskbarTextSize; BackToWindowsButton.MinHeight = taskbarCellHeight; BackToWindowsButton.MinWidth = buttonMinWidth; - BackToWindowsIcon.FontSize = taskbarIconSize; + BackToWindowsButton.Width = double.NaN; + BackToWindowsButton.Height = double.NaN; + ApplyBackToWindowsIconCircleSize(taskbarCellHeight); BackToWindowsTextBlock.FontSize = taskbarTextSize; - SetButtonContentSpacing(BackToWindowsButton, buttonContentSpacing); + RefreshBackToWindowsButtonPresentation(buttonContentSpacing); TaskbarProfileButton.Margin = new Thickness(0); TaskbarProfileButton.Padding = new Thickness(0); @@ -813,6 +830,156 @@ public partial class MainWindow : Window } } + private double GetBackToWindowsButtonMinWidth(double taskbarCellHeight) + { + return _backToWindowsButtonDisplayMode switch + { + BackToWindowsButtonDisplayModeIconOnly => taskbarCellHeight, + BackToWindowsButtonDisplayModeTextOnly => Math.Clamp(taskbarCellHeight * 1.8, 72, 260), + _ => Math.Clamp(taskbarCellHeight * 2.35, 100, 340) + }; + } + + private double GetBackToWindowsTaskbarCellHeight() + { + if (_currentDesktopCellSize > 0) + { + return Math.Clamp(_currentDesktopCellSize * 0.76, 36, 76); + } + + if (BackToWindowsButton.MinHeight > 0 && !double.IsNaN(BackToWindowsButton.MinHeight)) + { + return Math.Clamp(BackToWindowsButton.MinHeight, 36, 76); + } + + return 48; + } + + private double GetBackToWindowsContentSpacing(double taskbarCellHeight) + { + return Math.Clamp(taskbarCellHeight * 0.20, 6, 14); + } + + private void ApplyBackToWindowsIconCircleSize(double taskbarCellHeight) + { + var hitBoxSize = Math.Clamp(taskbarCellHeight * 0.62, 24, 44); + var iconSize = Math.Clamp(taskbarCellHeight * 0.32, 14, 24); + + BackToWindowsIconCircle.Width = hitBoxSize; + BackToWindowsIconCircle.Height = hitBoxSize; + BackToWindowsIconCircle.CornerRadius = new CornerRadius(hitBoxSize / 2d); + BackToWindowsIconHost.Width = hitBoxSize; + BackToWindowsIconHost.Height = hitBoxSize; + + if (BackToWindowsIconHost.Content is FluentIcon fluentIcon) + { + fluentIcon.FontSize = iconSize; + fluentIcon.Width = iconSize; + fluentIcon.Height = iconSize; + } + else if (BackToWindowsIconHost.Content is TextBlock textBlock) + { + textBlock.FontSize = Math.Clamp(taskbarCellHeight * 0.30, 12, 22); + } + } + + private static string NormalizeBackToWindowsIconSource(string? value) + { + return string.Equals(value, BackToWindowsIconSourceText, StringComparison.OrdinalIgnoreCase) + ? BackToWindowsIconSourceText + : BackToWindowsIconSourceFluentIcon; + } + + private static FluentIconKind NormalizeBackToWindowsFluentIcon(string? value) + { + return Enum.TryParse(value, ignoreCase: true, out var icon) + ? icon + : FluentIconKind.Circle; + } + + private static string NormalizeBackToWindowsIconText(string? value) + { + var normalized = string.IsNullOrWhiteSpace(value) + ? DefaultBackToWindowsIconText + : value.Trim(); + + var enumerator = StringInfo.GetTextElementEnumerator(normalized); + var builder = new System.Text.StringBuilder(); + var count = 0; + while (enumerator.MoveNext() && count < 4) + { + builder.Append(enumerator.GetTextElement()); + count++; + } + + return builder.Length > 0 ? builder.ToString() : DefaultBackToWindowsIconText; + } + + private void RefreshBackToWindowsIconContent(double taskbarCellHeight) + { + _backToWindowsIconSource = NormalizeBackToWindowsIconSource(_backToWindowsIconSource); + if (_backToWindowsIconSource == BackToWindowsIconSourceText) + { + BackToWindowsIconHost.Content = new TextBlock + { + Text = NormalizeBackToWindowsIconText(_backToWindowsIconText), + Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"), + FontWeight = FontWeight.SemiBold, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center + }; + ApplyBackToWindowsIconCircleSize(taskbarCellHeight); + return; + } + + BackToWindowsIconHost.Content = new FluentIcon + { + Icon = NormalizeBackToWindowsFluentIcon(_backToWindowsFluentIconName), + IconVariant = FluentIconVariant.Regular, + Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ApplyBackToWindowsIconCircleSize(taskbarCellHeight); + } + + private static string NormalizeBackToWindowsButtonDisplayMode(string? value) + { + return value switch + { + _ when string.Equals(value, BackToWindowsButtonDisplayModeIconOnly, StringComparison.OrdinalIgnoreCase) => + BackToWindowsButtonDisplayModeIconOnly, + _ when string.Equals(value, BackToWindowsButtonDisplayModeTextOnly, StringComparison.OrdinalIgnoreCase) => + BackToWindowsButtonDisplayModeTextOnly, + _ => BackToWindowsButtonDisplayModeIconAndText + }; + } + + private void RefreshBackToWindowsButtonPresentation(double? contentSpacing = null) + { + _backToWindowsButtonDisplayMode = NormalizeBackToWindowsButtonDisplayMode(_backToWindowsButtonDisplayMode); + var taskbarCellHeight = GetBackToWindowsTaskbarCellHeight(); + BackToWindowsButton.MinWidth = GetBackToWindowsButtonMinWidth(taskbarCellHeight); + RefreshBackToWindowsIconContent(taskbarCellHeight); + + var showIcon = _backToWindowsButtonDisplayMode is not BackToWindowsButtonDisplayModeTextOnly; + var showText = _backToWindowsButtonDisplayMode is not BackToWindowsButtonDisplayModeIconOnly; + + BackToWindowsIconCircle.IsVisible = showIcon; + BackToWindowsTextBlock.IsVisible = showText; + + if (BackToWindowsContentPanel is not null) + { + BackToWindowsContentPanel.Spacing = showIcon && showText + ? contentSpacing ?? GetBackToWindowsContentSpacing(taskbarCellHeight) + : 0; + } + + BackToWindowsButton.InvalidateMeasure(); + BackToWindowsButton.InvalidateArrange(); + } + private void UpdateComponentLibraryLayout(double cellSize) { if (ComponentLibraryWindow is null) diff --git a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml index e7413f3..1a2a9e7 100644 --- a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml @@ -65,6 +65,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml index 5bf7b2a..0644d42 100644 --- a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml @@ -56,6 +56,25 @@ + + + + + + + + + + + + + + + diff --git a/mocks/weather-widget-mock.html b/mocks/weather-widget-mock.html new file mode 100644 index 0000000..d68e104 --- /dev/null +++ b/mocks/weather-widget-mock.html @@ -0,0 +1,209 @@ + + + + + +天气组件 Mock V2 - 阑山桌面 + + + + +
+ + + | + + + +
+ +
Google Weather — 纯渐变,无装饰
+
+ +
Geometric — 径向渐变光晕 + 弧线段
+
+ +
Breezy — 径向渐变光晕 + 波浪线 + 弧线段
+
+ +
Lemon — 径向渐变光晕 + 天气场景装饰
+
+ + + +