From 1d415739424683587d53055180e4256ff5fd8949 Mon Sep 17 00:00:00 2001 From: Oksion Date: Sat, 17 Oct 2026 00:08:33 +0500 Subject: [PATCH] =?UTF-8?q?feat(osd):=20=D0=BF=D1=80=D0=BE=D0=B7=D1=80?= =?UTF-8?q?=D0=B0=D1=87=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=83=D0=B2=D0=B5?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9=200=E2=80=9380?= =?UTF-8?q?%=20(XIC-98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Настройки → Уведомления → «Прозрачность»: список «Нет / 10…80 %», выбор сразу показывает пример. По умолчанию 0 — как раньше. OsdTransparencyPercent в конфиге зажимается в 0..80. Затухание от базовой непрозрачности, шаг масштабируется — те же ~200 мс. OsdOpacity — чистая математика под тестами. RU/EN/ZH, README, CHANGELOG. GitHub #90. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +++ README.md | 2 +- README.ru.md | 2 +- src/Config/AppConfig.cs | 4 +++ src/Localization/lang/en.json | 3 ++ src/Localization/lang/ru.json | 3 ++ src/Localization/lang/zh.json | 3 ++ src/Ui/OsdForm.cs | 8 +++-- src/Ui/OsdPlacement.cs | 23 ++++++++++++++ src/Ui/Settings/OsdTab.cs | 14 +++++++++ src/Ui/SettingsActions.cs | 1 + src/Ui/TrayApp.cs | 3 ++ tests/XiControl.Tests/OsdOpacityTests.cs | 40 ++++++++++++++++++++++++ 13 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 tests/XiControl.Tests/OsdOpacityTests.cs 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); + } +}