diff --git a/CHANGELOG.md b/CHANGELOG.md
index acba923..1603b72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,10 @@
на смену питания — у сети и батареи режим свой. Пока служба Xiaomi запущена, переключатель
неактивен: она делает то же сама. Выключили — режим питания возвращается в сбалансированный,
ручной выбор в параметрах Windows не трогаем. По умолчанию выключено (XIC-96).
+- **Прозрачность уведомлений.** Настройки → Уведомления → «Прозрачность»: от «Нет» до 80 %
+ шагом 10. Выбор сразу показывает пример, затухание идёт от выбранного уровня так же плавно.
+ По умолчанию уведомления непрозрачные, как раньше. Просили в
+ [#90](https://github.com/Oksion/XiControl/issues/90) (XIC-98).
### Исправлено
- **Вебхук «заряд дошёл до порога» больше не обрывает «В дорогу».** Во время разового заряда до
diff --git a/README.md b/README.md
index dba40e2..d65a4cd 100644
--- a/README.md
+++ b/README.md
@@ -279,7 +279,7 @@ hidden if refresh-rate control is off), Touchpad (bottom dead zone, edge sliders
strength and click force, force press), Performance (mode visibility,
separately for AC and battery, with a switch above the list; startup mode, power profiles),
Keys (remapping), Notifications (where popups appear, how long
-they stay, which lock-key popups to show), HTTP API (port, token, permissions), Drivers (check with Xiaomi on a button) and About
+they stay, transparency, which lock-key popups to show), HTTP API (port, token, permissions), Drivers (check with Xiaomi on a button) and About
(version, model with the board code, BIOS, serial number — masked by default, click to reveal).
The quick toggles (charge, "travel", owl, refresh rate, Monitor,
mode) stay in the tray menu and the panel.
diff --git a/README.ru.md b/README.ru.md
index cba11f1..eec22b4 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -283,7 +283,7 @@ dotnet publish src/XiControl.csproj -c Release -r win-x64 --self-contained -p:Pu
Тачпад (мёртвая зона у нижнего края, края-ползунки, сила вибрации и усилие нажатия, сильное нажатие),
Производительность (видимость режимов — отдельно для сети и для батареи, переключатель наверху
списка; режим при старте, профили питания), Клавиши
-(переназначение), Уведомления (положение подсказок на экране, сколько висят, какие из
+(переназначение), Уведомления (положение подсказок на экране, сколько висят, прозрачность, какие из
клавиш-фиксаторов показывать), HTTP API (порт, токен, разрешения), Драйверы (проверка у Xiaomi по кнопке) и О программе (версия, модель с кодом
платы, BIOS, серийный номер — по умолчанию замаскирован, клик раскрывает). Быстрые
переключатели (заряд, «В дорогу», сова, герцовка, «Монитор», режим) остаются в меню трея и панели.
diff --git a/src/Config/AppConfig.cs b/src/Config/AppConfig.cs
index 8ff21df..b8d98ca 100644
--- a/src/Config/AppConfig.cs
+++ b/src/Config/AppConfig.cs
@@ -483,6 +483,10 @@ public sealed class AppConfig
/// Сколько OSD висит до затухания. Дефолт 2800 мс («Авто»-режим — на 600 мс дольше).
public int OsdDurationMs { get; set; } = 2800;
+ /// Прозрачность OSD, % (XIC-98): 0 — непрозрачная карточка (по умолчанию), до 80.
+ /// Правленное руками зажимается в 0..80 при применении ().
+ public int OsdTransparencyPercent { get; set; } = 0;
+
///
/// Где на экране показывать OSD. — как было всегда:
/// по центру ширины, чуть ниже середины по высоте. Остальные восемь позиций прижимают
diff --git a/src/Localization/lang/en.json b/src/Localization/lang/en.json
index 1e34b84..6f62a75 100644
--- a/src/Localization/lang/en.json
+++ b/src/Localization/lang/en.json
@@ -377,6 +377,9 @@
"settings.osd.duration": "How long it stays",
"settings.osd.duration.desc": "Time before the popup starts to fade.",
"settings.osd.duration.sec": "{0:0.#} s",
+ "settings.osd.transparency": "Transparency",
+ "settings.osd.transparency.desc": "How much of what is underneath shows through the notification. Picking a value shows an example right away.",
+ "settings.osd.transparency.none": "None",
"settings.osd.preview": "Try it",
"settings.osd.preview.desc": "Show a popup right now — position and duration as they will be.",
"settings.osd.preview.btn": "Show example",
diff --git a/src/Localization/lang/ru.json b/src/Localization/lang/ru.json
index 662b95b..1a9d5ba 100644
--- a/src/Localization/lang/ru.json
+++ b/src/Localization/lang/ru.json
@@ -377,6 +377,9 @@
"settings.osd.duration": "Сколько висит",
"settings.osd.duration.desc": "Время до того, как подсказка начнёт гаснуть.",
"settings.osd.duration.sec": "{0:0.#} с",
+ "settings.osd.transparency": "Прозрачность",
+ "settings.osd.transparency.desc": "Насколько сквозь уведомление просвечивает то, что под ним. При выборе сразу показывается пример.",
+ "settings.osd.transparency.none": "Нет",
"settings.osd.preview": "Проверить",
"settings.osd.preview.desc": "Показать подсказку прямо сейчас — видно положение и длительность.",
"settings.osd.preview.btn": "Показать пример",
diff --git a/src/Localization/lang/zh.json b/src/Localization/lang/zh.json
index a41028d..4ebfb47 100644
--- a/src/Localization/lang/zh.json
+++ b/src/Localization/lang/zh.json
@@ -377,6 +377,9 @@
"settings.osd.duration": "显示时长",
"settings.osd.duration.desc": "提示开始淡出前停留的时间。",
"settings.osd.duration.sec": "{0:0.#} 秒",
+ "settings.osd.transparency": "透明度",
+ "settings.osd.transparency.desc": "通知下方的内容透出多少。选择后会立即显示示例。",
+ "settings.osd.transparency.none": "无",
"settings.osd.preview": "试一下",
"settings.osd.preview.desc": "立即显示一次提示——位置和时长与实际一致。",
"settings.osd.preview.btn": "显示示例",
diff --git a/src/Ui/OsdForm.cs b/src/Ui/OsdForm.cs
index 06f4e2d..f5ac5fc 100644
--- a/src/Ui/OsdForm.cs
+++ b/src/Ui/OsdForm.cs
@@ -29,6 +29,10 @@ public sealed class OsdForm : Form
/// Где показывать карточку (сетка 3×3). Настраивается на вкладке «Уведомления»,
/// TrayApp выставляет её на старте и после каждой правки.
public OsdPosition Position { get; set; } = OsdPosition.Center;
+
+ // Прозрачность карточки (XIC-98): с какой непрозрачности показываем и с какой гасим.
+ private double _baseOpacity = 1.0;
+ public void SetTransparency(int percent) => _baseOpacity = OsdOpacity.Base(percent);
private int DisplayMsAuto => _displayMs + 600;
private readonly System.Windows.Forms.Timer _display = new() { Interval = 2800 };
@@ -69,7 +73,7 @@ public OsdForm()
_display.Tick += (_, _) => { _display.Stop(); _fade.Start(); };
_fade.Tick += (_, _) =>
{
- Opacity -= 0.08;
+ Opacity -= OsdOpacity.FadeStep(_baseOpacity); // от базовой — те же ~200 мс, без скачка
if (Opacity <= 0.01) { _fade.Stop(); _gauge.Stop(); Hide(); }
};
_gauge.Tick += (_, _) => { _gaugeT += 0.03f; Invalidate(); };
@@ -135,7 +139,7 @@ public void Flash(OsdKind kind, string title, string? subtitle = null, ChargeBad
Location = OsdPlacement.Locate(wa, new Size(w, h), Position, Sc(OsdPlacement.Margin));
_display.Stop(); _fade.Stop();
- Opacity = 1.0;
+ Opacity = _baseOpacity;
_gauge.Stop();
// для «Авто» показываем дольше — стрелка успевает плавно «настроиться»
_display.Interval = kind == OsdKind.Auto ? DisplayMsAuto : _displayMs;
diff --git a/src/Ui/OsdPlacement.cs b/src/Ui/OsdPlacement.cs
index 3fda41c..f0a1949 100644
--- a/src/Ui/OsdPlacement.cs
+++ b/src/Ui/OsdPlacement.cs
@@ -90,3 +90,26 @@ public static Screen TargetScreen()
[DllImport("user32.dll")]
private static extern nint GetForegroundWindow();
}
+
+///
+/// Прозрачность OSD (XIC-98, просили в GitHub #90). Чистая математика — под тестами; сама форма
+/// только применяет. По умолчанию 0 — карточка непрозрачна, как всегда.
+///
+public static class OsdOpacity
+{
+ /// Потолок: при 80% карточка видна хотя бы на пятую часть.
+ public const int MaxPercent = 80;
+
+ /// Шаг затухания непрозрачной карточки за тик (16 мс) — исторический, ~200 мс до нуля.
+ public const double FullFadeStep = 0.08;
+
+ /// Процент из config.json — его правят руками, поэтому зажимаем в 0..80.
+ public static int Clamp(int percent) => Math.Clamp(percent, 0, MaxPercent);
+
+ /// С какой непрозрачности карточка показывается: 40% прозрачности → 0.6.
+ public static double Base(int percent) => 1.0 - Clamp(percent) / 100.0;
+
+ /// Шаг затухания от базовой непрозрачности: пропорционально меньше, чтобы
+ /// полупрозрачная карточка гасла те же ~200 мс, а не быстрее непрозрачной.
+ public static double FadeStep(double baseOpacity) => FullFadeStep * baseOpacity;
+}
diff --git a/src/Ui/Settings/OsdTab.cs b/src/Ui/Settings/OsdTab.cs
index 73b3316..2a92bb6 100644
--- a/src/Ui/Settings/OsdTab.cs
+++ b/src/Ui/Settings/OsdTab.cs
@@ -30,6 +30,9 @@ public OsdTab(SettingsToolkit ui, AppConfig cfg, SettingsActions act, Action reb
ui.AddRow(this, "settings.osd.duration", "settings.osd.duration.desc",
DurationCombo(ui, cfg.OsdDurationMs, act.SetOsdDuration));
+ ui.AddRow(this, "settings.osd.transparency", "settings.osd.transparency.desc",
+ TransparencyCombo(ui, cfg.OsdTransparencyPercent, act.SetOsdTransparency));
+
// ширину меряем сами: AutoSize у кнопки срабатывает позже, чем карточка считает
// раскладку, и кнопка выходит микроскопической (та же грабля, что в DisplayTab)
var preview = ui.LinkButton("settings.osd.preview.btn", act.PreviewOsd);
@@ -53,6 +56,17 @@ private void AddLock(SettingsToolkit ui, SettingsActions act, AppConfig cfg, Loc
=> ui.AddRow(this, titleKey, titleKey + ".desc",
ui.Toggle(!cfg.HiddenLockOsd.Contains(key), on => act.SetLockOsd(key, on)));
+ /// Прозрачность списком, как остальные проценты в настройках (XIC-98): «Нет», 10…80%.
+ /// Значение из config.json вне шага (правили руками) не теряем — добавляем его, уже зажатое.
+ private static ComboBox TransparencyCombo(SettingsToolkit ui, int current, Action apply)
+ {
+ current = OsdOpacity.Clamp(current);
+ int[] steps = [.. Enumerable.Range(0, OsdOpacity.MaxPercent / 10 + 1).Select(i => i * 10)];
+ int[] items = steps.Contains(current) ? steps : [.. steps.Append(current).Order()];
+ string[] names = [.. items.Select(p => p == 0 ? Loc.T("settings.osd.transparency.none") : $"{p}%")];
+ return ui.Combo(names, Array.IndexOf(items, current), i => apply(items[i]), ui.Sc(150));
+ }
+
/// Длительность в секундах — человек думает в них, а не в миллисекундах.
/// Значение из config.json вне списка (правили руками) не теряется: добавляем его как есть.
private static ComboBox DurationCombo(SettingsToolkit ui, int current, Action apply)
diff --git a/src/Ui/SettingsActions.cs b/src/Ui/SettingsActions.cs
index 22d220f..de4181e 100644
--- a/src/Ui/SettingsActions.cs
+++ b/src/Ui/SettingsActions.cs
@@ -87,6 +87,7 @@ public sealed class SettingsActions
public required Action TrayMetricApplied; // индикатор в трее (XIC-35): вкл/выкл/метрика/период изменились
public required Action SetOsdPosition; // где показывать OSD (сетка 3×3)
public required Action SetOsdDuration; // сколько висит, мс
+ public required Action SetOsdTransparency; // прозрачность OSD, % (0 — нет; XIC-98)
public required Action SetLockOsd; // показывать ли плашку фиксатора
public required Action PreviewOsd; // «Показать пример» — настоящий OSD настоящим путём
}
diff --git a/src/Ui/TrayApp.cs b/src/Ui/TrayApp.cs
index e9e761e..460690a 100644
--- a/src/Ui/TrayApp.cs
+++ b/src/Ui/TrayApp.cs
@@ -852,6 +852,8 @@ private void OpenSettings()
TrayMetricApplied = TrayMetricApplied,
SetOsdPosition = p => { _cfg.OsdPosition = p; _cfg.Save(); OsdApplied(); },
SetOsdDuration = ms => { _cfg.OsdDurationMs = ms; _cfg.Save(); OsdApplied(); },
+ // прозрачность сразу видна на примере — выбирают глазами (XIC-98)
+ SetOsdTransparency = pct => { _cfg.OsdTransparencyPercent = OsdOpacity.Clamp(pct); _cfg.Save(); PreviewOsd(); },
SetLockOsd = SetLockOsd,
PreviewOsd = PreviewOsd,
};
@@ -894,6 +896,7 @@ private void OsdApplied()
{
_osd.DurationMs = _cfg.OsdDurationMs;
_osd.Position = _cfg.OsdPosition;
+ _osd.SetTransparency(_cfg.OsdTransparencyPercent);
}
// Показывать ли плашку этого фиксатора. Храним СКРЫТЫЕ (как HiddenModes у режимов):
diff --git a/tests/XiControl.Tests/OsdOpacityTests.cs b/tests/XiControl.Tests/OsdOpacityTests.cs
new file mode 100644
index 0000000..ef1e271
--- /dev/null
+++ b/tests/XiControl.Tests/OsdOpacityTests.cs
@@ -0,0 +1,40 @@
+using FluentAssertions;
+using XiControl.Ui;
+using Xunit;
+
+namespace XiControl.Tests;
+
+/// Прозрачность OSD (XIC-98): зажим ручной правки, базовая непрозрачность и затухание.
+public class OsdOpacityTests
+{
+ [Theory]
+ [InlineData(-10, 0)]
+ [InlineData(0, 0)]
+ [InlineData(40, 40)]
+ [InlineData(80, 80)]
+ [InlineData(95, 80)] // правили config.json руками — карточка не должна исчезнуть
+ public void Процент_зажимается_в_0_80(int raw, int expected) =>
+ OsdOpacity.Clamp(raw).Should().Be(expected);
+
+ [Fact]
+ public void По_умолчанию_карточка_непрозрачна() =>
+ OsdOpacity.Base(new Config.AppConfig().OsdTransparencyPercent).Should().Be(1.0);
+
+ [Theory]
+ [InlineData(40, 0.6)]
+ [InlineData(80, 0.2)]
+ [InlineData(200, 0.2)] // выше потолка — как 80%
+ public void Базовая_непрозрачность(int percent, double expected) =>
+ OsdOpacity.Base(percent).Should().BeApproximately(expected, 1e-9);
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(40)]
+ [InlineData(80)]
+ public void Затухание_длится_одинаково_при_любой_прозрачности(int percent)
+ {
+ // сколько тиков от базовой непрозрачности до нуля — столько же, сколько у непрозрачной
+ double b = OsdOpacity.Base(percent);
+ (b / OsdOpacity.FadeStep(b)).Should().BeApproximately(1.0 / OsdOpacity.FullFadeStep, 1e-9);
+ }
+}