From 9c4f8d748f0584a0c93c854171091fd570e2db50 Mon Sep 17 00:00:00 2001 From: Fantom TM Date: Sat, 26 Sep 2026 12:11:20 +0700 Subject: [PATCH] 0.9.0 --- App.xaml | 15 ++ App.xaml.cs | 90 +++++++ Assets/LockScreenLogo.scale-200.png | Bin 0 -> 432 bytes Assets/SplashScreen.scale-200.png | Bin 0 -> 5372 bytes Assets/Square150x150Logo.scale-200.png | Bin 0 -> 1755 bytes Assets/Square44x44Logo.scale-200.png | Bin 0 -> 637 bytes ...x44Logo.targetsize-24_altform-unplated.png | Bin 0 -> 283 bytes Assets/StoreLogo.png | Bin 0 -> 456 bytes Assets/Wide310x150Logo.scale-200.png | Bin 0 -> 2097 bytes Assets/app.ico | Bin 0 -> 410598 bytes HearingProtector.csproj | 75 +++++- HearingProtector.slnx | 12 +- MainPage.xaml | 109 +++++++++ MainPage.xaml.cs | 222 ++++++++++++++++++ MainWindow.xaml | 48 ++++ MainWindow.xaml.cs | 62 +++++ MicaWindowExt.cs | 144 ++++++++++++ Package.appxmanifest | 53 +++++ Program.cs | 100 +++----- Properties/launchSettings.json | 7 + RelayCommand.cs | 21 ++ SettingsStore.cs | 62 +++++ app.manifest | 19 ++ 23 files changed, 965 insertions(+), 74 deletions(-) create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 Assets/LockScreenLogo.scale-200.png create mode 100644 Assets/SplashScreen.scale-200.png create mode 100644 Assets/Square150x150Logo.scale-200.png create mode 100644 Assets/Square44x44Logo.scale-200.png create mode 100644 Assets/Square44x44Logo.targetsize-24_altform-unplated.png create mode 100644 Assets/StoreLogo.png create mode 100644 Assets/Wide310x150Logo.scale-200.png create mode 100644 Assets/app.ico create mode 100644 MainPage.xaml create mode 100644 MainPage.xaml.cs create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cs create mode 100644 MicaWindowExt.cs create mode 100644 Package.appxmanifest create mode 100644 Properties/launchSettings.json create mode 100644 RelayCommand.cs create mode 100644 SettingsStore.cs create mode 100644 app.manifest diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..0b84abf --- /dev/null +++ b/App.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..06e6476 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,90 @@ +using H.NotifyIcon; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; +using System; +using System.IO; +using System.Reflection; + +namespace HearingProtector; + +public partial class App : Application +{ + // ← ДОБАВИТЬ: ссылка на единственный экземпляр App + public static App? CurrentApp { get; private set; } + + public Window? MainWindow { get; private set; } + + private TaskbarIcon? _trayIcon; + + public App() + { + CurrentApp = this; // ← ДОБАВИТЬ до InitializeComponent() + InitializeComponent(); + } + + public static string Version { get; private set; } = "?"; + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + Version = typeof(App).Assembly + .GetCustomAttribute()? + .InformationalVersion ?? "?"; + + MainWindow = new MainWindow(); + + var openItem = new MenuFlyoutItem + { + Text = "Открыть", + Command = new RelayCommand(OpenFromTray) + }; + + var exitItem = new MenuFlyoutItem + { + Text = "Выход", + Command = new RelayCommand(ExitApp) + }; + + var menu = new MenuFlyout(); + menu.Items.Add(openItem); + menu.Items.Add(new MenuFlyoutSeparator()); + menu.Items.Add(exitItem); + + _trayIcon = new TaskbarIcon + { + ToolTipText = $"Hearing Protector v{Version}", + IconSource = new BitmapImage(new Uri( + Path.Combine(AppContext.BaseDirectory, "Assets", "app.ico"))), + ContextFlyout = menu, + MenuActivation = H.NotifyIcon.Core.PopupActivationMode.RightClick, + NoLeftClickDelay = true, + DoubleClickCommand = new RelayCommand(OpenFromTray) + }; + + _trayIcon.ForceCreate(); + + MainWindow.Activate(); + } + + private void OpenFromTray() + { + MainWindow?.DispatcherQueue.TryEnqueue(() => + { + (MainWindow as MainWindow)?.RestoreFromTray(); + }); + } + + private void ExitApp() + { + MainWindow?.DispatcherQueue.TryEnqueue(() => + { + if (MainWindow is MainWindow mw) + mw.ReadyToClose = true; + + _trayIcon?.Dispose(); + _trayIcon = null; + + Exit(); + }); + } +} \ No newline at end of file diff --git a/Assets/LockScreenLogo.scale-200.png b/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..7440f0d4bf7c7e26e4e36328738c68e624ee851e GIT binary patch literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezr3(FqV6|IEGZ*x-#9g>~Mkr+x6^F zy~CDX2QIMs&Gcs3RnRBoxBA!*(Mfw0KTCYuYk0WlEIV>qBmPl! zq4ukrvfADX@#p8fbLY(H47N+k`FZ(FZh?cDro7>{8mkBO3>^oaIx`3!Jl)Qq)HI!+ z(S=1{o~eT)&W^=Ea8C`-17(Jv5(nHFJ{dOjGdxLVkY_y6&S1whfuFI4MM0kF0f&cO zPDVpV%nz;Id$>+0Ga5e9625-JcI)oq=#Pa3p^>8BB}21BUw@eN!-6@w%X+^`+Vn?! zryu|3T>kVWNBYyBc=7Y6H#s1Ah!OI_nezW zXTqOdkv2Az6KKBV=$yHdF^R3Fqw(TZEoNSZX>reXJ#bwX42%f|Pgg&ebxsLQ010xn AssI20 literal 0 HcmV?d00001 diff --git a/Assets/SplashScreen.scale-200.png b/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..32f486a86792a5e34cd9a8261b394c49b48f86be GIT binary patch literal 5372 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 literal 0 HcmV?d00001 diff --git a/Assets/Square44x44Logo.scale-200.png b/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000000000000000000000000000000000..f713bba67f551ef91020b75716a4dc8ebd744b1c GIT binary patch literal 637 zcmeAS@N?(olHy`uVBq!ia0vp^5g^RL1|$oo8kjIJFu8cTIEGZ*dUI*J;2{SImxtDO zm%3!R$UazoY}x{$j0P5ABYXWr(l=jxJ6ps1W{tV=^>{Dl><3nv3A}sm=EZ)#l3`NR zpZda3^rNox*D1%NC98Z~L*6zipLw~Gxn&(Y-;KmJ+aR6eLabU-L#y8HW%7P-E_-VlLqIabbHPHKT*)fT@9iWJ7iWgOT9%0}Lrj>lztPxWq6sPw3pi z#-<=#$jjrP_DD*i!RLsn0mIA=>4~N)IMYWIf=j%-zuKCdMG%tHYot70D1| zvWa0wMhauW#S>1CnI_;>!1Q3zMA17@DOVq{MQ+{U7^a&yA+%dMCG;WNPV0i;w$tu; zX^b}UKziPM)(<;)ruW;-`)bBN+rQNM*Zs_>?n$|FVFo-e*PZb*@U7VAd+tHb4e?=Blc~}S6K)wL}r*Gf`BM#QB z+y>N$mCswb4d{^{S9v_!eQj4fTRMOwOCi?lSk9%<=vAz}jM-*PQtH@Odn1LZcd^j#o> hW$4xn+CT+ep9lJ{OAO?njobhL002ovPDHLkV1nYebbkN< literal 0 HcmV?d00001 diff --git a/Assets/StoreLogo.png b/Assets/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..a4586f26bdf7841cad10f39cdffe2aca3af252c1 GIT binary patch literal 456 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fF!p=8IEGZ*dUM0H=rDtTTVkd2 z(%lbKn@VS_lUaADVB&;Z6F#LM+mPsa?e>FnHo;HND^!P`-lX%BH~FOg%y&x+t*x!? zg$#_1A1kgsSvO(fw`bOmo;lrJX8byO1j^gf7qohR%mmt z@L)WX;>gqgK|tWJvQ5j;4;=gt4HXVKSMYRv5RhY5vS~TqfK_NAP*r{h!!g^BZ;w4r z7CGdsai)y;fJQc`7{Zc2b==h%o`Op$|bg6a&nL{*m7-=0>k4M4-PXlU;G-?%*(*g>iFt^ U$m#7DfHB12>FVdQ&MBb@0G`#n8vpc0sq%A~kJcD9FY~qQRMt?ZR3YyDZt}Od;|mgpc{2dv9AHF){kXU%k({ z=Y8JidEayHTkG@twPZ|U3_^%3ct-OgLSiFAqDN!|tbCX@c@?4P`2x*TMK!+Q4b?k0 ziW7!!KF6dPWcF<%I|iznM~`QJ_V7sHGV_D`dhgpA9Vd@&X}ErK+j~_rdv;Bp?OA@a zFXOk7eWOJe5NcK;6h$FaM&7JxNc#-@QTwzW6x#d_zmQNkz5) zPI;kh;3d;5UCJU+9a(cOxX(|edWoOiAEdGU#kPJ&xnc2||3vDbuhBCkj-pb0as$Zl z5;}4n=**n6(1g`JEtSy;SG6X;#-F~Oz3lESG2b5`j@wAwY4Yp<=4Xeb>iH=6aicF?DxD&q{`!&}ct zBI)aycwuobQAf&678Uf+Mmh-@9RUhyH~>?w0dixO0#jZjEc9R^=5NZw=|a(kcB?9^ zfnTiEFXp-q#B;Tn>(O%$A*ud^Rg&eVH6Y_5Y%!E39RR&s?XpG`gKwU!6FE1 z7X)DC7)*(5g}lh`4`{i~DZcWupZI`K)_4P)VE{@gc7@Xsd^86zl~_mOYH?I4!aGeX z^E(_=L6?PgveDQ+r%P@UISEXrkn`LHJZ##+!-anV>6h)IkKp;E@p8+3&(5%kS2)ld*J*rJccZM0iyaAx7+F~GW1UWFK&3X$PE1^}NH zgAG9ck5K!{07OwU@j@Do>TbH=CDEo#4m0cEyAuXy_<&jlzJVcKweSJ5 z&=q~iIn18$w8yb=rmEmHxVEUA^?RwnB?6Qlp1os8@*dWTGL2bhzZ!s*xqScR?EPL` zo(JwNdKUUYy7GtvZ3asXm)cgFvCx9EmAi;|w=a0iGiv%%VYKh`P0Wma4y`Xyx|T~( zAmfGbgbEEC7)j8b@WA@+5W3a61HJXC1dX@6_T|Czk0I0zBk%tnW~()VWITGI!`$c< gARL?UBrYYkwoDw4eo*CrzXGTrZ@;GF>596)00d&n@&Et; literal 0 HcmV?d00001 diff --git a/Assets/app.ico b/Assets/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..1376cc0a301c73b17d931108a0b6b7c522bbf9d6 GIT binary patch literal 410598 zcmeHw2Yg(`wf@KzFdae(BsMjaKT{zjs#Z%!7uhz4o&HzIj`vnvWl*cE)|C_s=2t{?uXW5QUHf8f4w)pGx7O z1d)J9KqMd%5DAC`L;@lKk$^}*Bp?zH35Wzl0wMvCfJi_jAQBJ>hy+9eA_0+rNI)bY z5)cW91VjQN0g-@6KqMd%5DAC`L;@lKk$^}*Bp?zH35Wzl0wMvCfJi_jkRb`IxJoI~ z%Ks=;evMMa#y@Y`e;k zSxd4GC1C3*fd3ree-Q9L7}y^I8d{jEh5`G-LEA#_+mV2Op#dlZTmSf_Eo=kK55wONotZ{qh! z*v?(}oeH`F*MEYI{10sCB-qdqv?&m6Yj@y(`(m^c{_r|%a6+KY3ARmKOK?{Lq4)=m z)q+W=<1bc&fd9RL?-QZ3^FfoKqensSg1!PR0xbjiK&7Bc&??Y6&_+;0fEw{DG;m~K z>wOdSPFtW&tOS*TXe+dtZ$Y#n+R|g7$)F2>|C50Kz4aw(&_dWCY_EGv$+lB>t$$#} zrt^U7Ta(xL_N5n zUx`EHn5Ye~4@q03P0}_wmi!CY?5JwYJ1vC`qfe^vw_5iT^jYEWleSKhV8-s$%D-R? z^&;T?dZj*p0rI+54K5g~w)2luyFhM-qb%iE%tIjd%UJ$)f>?^lDKggn0hacqWdd$;r^@rW!_VFN7zTm8b=E^Rnhom@Cp?MB;P zKFz|uK6_x}--o$7QW54)mg9FJ=2U&7)%I9(aR|!F-$Qmcfj$RG8((mm3Y?}sPTKA` zQ+M5r-`~@=i>_58zMhP>F6IqNY4ZUb477HDop09vrDIf1*%-4v?-91GMP{pzAOm@N?e;wJWNAh4BI9M;q9Wwg9QW2H@N5 zwXyMEc%_*m83sA*UN9c(E_2n>peo3R{ipt+c@b@_(wh!iFsT;UJXJXcR)GtBhWu^?_E$6s6Xmj@+5kVWCOJM zH-U?bK|drWmglNLxM#3Jy!4mAypRHA+Yb2sDe`$8XbEUDNapbcCy{W{U+>fAx%Ys3 z45kwkpdE=38!y?8f6i}^ia<+{uab#a*ORLb#d@ebE@FY0 zAU22*VuhHo@sq9PfXzS4H2B~c0{Sl6{O2LB^Fi$EOT8}`ae|Tl{7x6KKui!D#0arM z%-HzJHvH>6tZ@Quz&eL6{C);M2_@szPFNFp9`g7SXuZ_?((Vx(2(kfU0@xr%h!tXn z*dc~&JgIC6Slg^Xi!raU-|x_t!Po}Mv>zjXj|Z?XYkIs;?~jB1k-Wf*@X%qf0gMnU z#0;@R3=vC)vIPGZUZ@o5E9~R@`=8Bz=#$aDxd(Q}{X2sFOxil|j=dE+e@qDBo|FX{ zFAIqcpTWvFbuzI-4B2?f2KZwwkMXyQGE9}i&nC*T17TO=kf#FVP4@4k8~cGeo|q@T zeUrD-#W(Jw%kKV>uDJhXL%c4&abNR{@7V83d61FvpxIyrm?3sbEDTxe1c5ED2jU-n z7@J1n`rp9)`yl$(lD1|>WRRl^Ck)oUDI;|0oj=qyFJ7oOd^J-y7QJI=!&kG+JwD?* ze77JsM|71DT^WoJEAJCKppiD7vSEEN@n0|j>lh}e5y-2LD~ap zwP9lBb*u$AqjZAWt{9lⓈ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 - - Exe - net10.0-windows - enable + WinExe + net10.0-windows10.0.17763.0 + 10.0.17763.0 + HearingProtector + x64 + win-x64 + true + latest + None + false enable - true - true - HearingProtector.Program + + + x64 + true + true + true + true + true + true + + + 0.9.0 + 0.9.0.0 + 0.9.0.0 + 0.9.0 + Hearing Protector + Hearing Protector + QwertyKez + Copyright © 2026 + + + $(DefineConstants);DISABLE_XAML_GENERATED_MAIN + + + app.manifest + + + + + + + + + + + - + + + MSBuild:Compile + + + + + + False + True + + False + + true + + Assets\app.ico + + \ No newline at end of file diff --git a/HearingProtector.slnx b/HearingProtector.slnx index 57f5810..2060d3b 100644 --- a/HearingProtector.slnx +++ b/HearingProtector.slnx @@ -1,3 +1,13 @@ - + + + + + + + + + + + diff --git a/MainPage.xaml b/MainPage.xaml new file mode 100644 index 0000000..ab6cf7f --- /dev/null +++ b/MainPage.xaml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MainPage.xaml.cs b/MainPage.xaml.cs new file mode 100644 index 0000000..cac4ec7 --- /dev/null +++ b/MainPage.xaml.cs @@ -0,0 +1,222 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; // RangeBaseValueChangedEventArgs +using NAudio.CoreAudioApi; +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace HearingProtector; + +public sealed partial class MainPage : Page +{ + // ─── Константы ──────────────────────────────────────────────────────── + private const int PollMs = 1000; + private const float UserChangeEps = 0.005f; + private const float WriteEps = 0.0005f; + + // Технический пол для экспоненциальной формулы. Нужен только когда + // пользователь выбрал целевой минимум 0% (иначе exp не работает). + private const double FormulaFloor = 0.001; // 0.1% + + // ─── Config ─────────────────────────────────────────────────────────── + private sealed record Config(double MinPercent, double FullDecayMinutes) + { + public double MinVolume => MinPercent / 100.0; // реальная цель + public double FormulaMin => Math.Max(MinVolume, FormulaFloor);// >= 0.1% + public double TotalTicks => FullDecayMinutes * 60_000.0 / PollMs; + public double DecayPerTick => Math.Pow(FormulaMin, 1.0 / TotalTicks); + public double DbPerTick => 20.0 * Math.Log10(DecayPerTick); + } + + // ─── Поля ───────────────────────────────────────────────────────────── + private volatile Config? _config; + private CancellationTokenSource? _cts; + private bool _initialized; + + // ─── Конструктор ────────────────────────────────────────────────────── + public MainPage() + { + InitializeComponent(); + + ApplySettings(SettingsStore.Load()); + UpdateLabels(); // слайдеры могут сами не дёрнуть ValueChanged после загрузки + _initialized = true; + + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + // ─── Жизненный цикл ─────────────────────────────────────────────────── + private void OnLoaded(object sender, RoutedEventArgs e) + { + _config ??= BuildConfig(); + + if (_cts is not null) return; + _cts = new CancellationTokenSource(); + _ = RunAsync(_cts.Token); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + } + + // ─── Настройки ──────────────────────────────────────────────────────── + private void ApplySettings(AppSettings s) + { + MinVolumeSlider.Value = s.MinPercent; + FullDecaySlider.Value = s.FullDecayMinutes; + } + + private AppSettings CurrentSettings() => + new(MinVolumeSlider.Value, FullDecaySlider.Value); + + private void Config_ValueChanged(object sender, RangeBaseValueChangedEventArgs e) + { + if (!_initialized) return; + UpdateLabels(); + _config = BuildConfig(); + SettingsStore.Save(CurrentSettings()); + } + + private void UpdateLabels() + { + if (MinVolumeLabel is null || FullDecayLabel is null) return; + + MinVolumeLabel.Text = $"{MinVolumeSlider.Value:F0} %"; + + int m = (int)FullDecaySlider.Value; + FullDecayLabel.Text = m switch + { + < 60 => $"{m} мин", + _ when m % 60 == 0 => $"{m / 60} ч", + _ => $"{m / 60} ч {m % 60} мин", + }; + } + + private Config BuildConfig() + { + var s = CurrentSettings(); + double min = s.MinPercent; + double mins = s.FullDecayMinutes; + + if (double.IsNaN(min) || min < 0) min = 1; + if (double.IsNaN(mins) || mins < 30) mins = 30; + + return new Config(min, mins); + } + + // ─── Рабочий цикл ───────────────────────────────────────────────────── + private async Task RunAsync(CancellationToken ct) + { + using var enumerator = new MMDeviceEnumerator(); + using var device = enumerator.GetDefaultAudioEndpoint( + DataFlow.Render, Role.Multimedia); + var volume = device.AudioEndpointVolume; + + float baseline = volume.MasterVolumeLevelScalar; + float current = baseline; + + while (!ct.IsCancellationRequested) + { + var cfg = _config; + if (cfg is null) + { + try { await Task.Delay(PollMs, ct); } + catch (OperationCanceledException) { break; } + continue; + } + + double etaSec = double.NaN; + bool muted = false; + bool atFloor = false; + double percent = current * 100; + + try + { + float actual = volume.MasterVolumeLevelScalar; + + // Ручное вмешательство → синхронизация + if (Math.Abs(actual - baseline) > UserChangeEps) + { + current = actual; + baseline = actual; + } + + muted = volume.Mute; + atFloor = current <= cfg.MinVolume + 0.0005f; + + if (!muted && !atFloor) + { + if (current > cfg.FormulaMin + 0.0005f) + // Экспоненциальный шаг до технического пола + current = (float)Math.Max(cfg.FormulaMin, + current * cfg.DecayPerTick); + else + // Пол достигнут, а цель ниже (только кейс MinPercent = 0) + current = (float)cfg.MinVolume; + + if (Math.Abs(actual - current) > WriteEps) + volume.MasterVolumeLevelScalar = current; + + baseline = current; + + var logK = Math.Log(cfg.DecayPerTick); + if (logK < 0 && current > cfg.MinVolume) + etaSec = Math.Log(cfg.MinVolume / current) + / logK * PollMs / 1000.0; + } + + percent = current * 100; + } + catch { /* переживаем транзиентные COM-сбои */ } + + UpdateStatus(percent, muted, atFloor, etaSec, cfg); + + try { await Task.Delay(PollMs, ct); } + catch (OperationCanceledException) { break; } + } + } + + // ─── UI ─────────────────────────────────────────────────────────────── + private void UpdateStatus(double percent, bool muted, bool atFloor, + double etaSec, Config cfg) + { + DispatcherQueue.TryEnqueue(() => + { + VolumeBar.Value = percent; + + if (muted) + StatusText.Text = "🔇 Звук выключен — пауза"; + else if (atFloor && cfg.MinPercent == 0) + StatusText.Text = "🔇 Достигнута тишина"; + else if (atFloor) + StatusText.Text = $"Уровень достиг минимума ({cfg.MinPercent:F0}%)"; + else + StatusText.Text = $"Громкость: {percent:F2}%"; + + if (muted) + EtaText.Text = "Снимите мьют — цикл продолжится"; + else if (atFloor) + EtaText.Text = "Увеличьте громкость, чтобы возобновить цикл"; + else if (!double.IsNaN(etaSec)) + { + var t = TimeSpan.FromSeconds(Math.Max(0, etaSec)); + EtaText.Text = t.TotalHours >= 1 + ? $"До минимума ≈ {(int)t.TotalHours}ч {t.Minutes:D2}м" + : $"До минимума ≈ {t.Minutes:D2}м {t.Seconds:D2}с"; + } + else + EtaText.Text = ""; + + RateText.Text = + $"Темп: {cfg.DbPerTick:F4} дБ/тик · " + + $"период {PollMs} мс · цель {cfg.MinPercent:F0}% за " + + $"{cfg.FullDecayMinutes:F0} мин от 100%"; + }); + } +} \ No newline at end of file diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..f717a6b --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs new file mode 100644 index 0000000..148db58 --- /dev/null +++ b/MainWindow.xaml.cs @@ -0,0 +1,62 @@ +using HearingProtector.Extensions; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; + +namespace HearingProtector; + +public sealed partial class MainWindow : Window +{ + // Флаг: true, когда пользователь выбрал «Выход» в трее + internal bool ReadyToClose { get; set; } + + public MainWindow() + { + InitializeComponent(); + this.SetMicaStyle(AppTitleBar, new(400, 300), false, false); + + VersionText.Text = $"v{App.Version}"; + + AppWindow.Changed += AppWindow_Changed; + AppWindow.Closing += AppWindow_Closing; + } + + private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args) + { + if (!args.DidPresenterChange) return; + if (sender.Presenter is OverlappedPresenter p && + p.State == OverlappedPresenterState.Minimized) + { + MinimizeToTray(); + } + } + + private void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args) + { + // Если пользователь явно выбрал «Выход» — пропускаем закрытие насквозь. + if (ReadyToClose) return; + + // Иначе крестик/Alt+F4 сворачивают окно в трей, а не закрывают приложение. + args.Cancel = true; + MinimizeToTray(); + } + + public void MinimizeToTray() + { + AppWindow.IsShownInSwitchers = false; + AppWindow.Hide(); + } + + public void RestoreFromTray() + { + AppWindow.Show(); + AppWindow.IsShownInSwitchers = true; + + if (AppWindow.Presenter is OverlappedPresenter p && + p.State == OverlappedPresenterState.Minimized) + { + p.Restore(); + } + + Activate(); + } +} \ No newline at end of file diff --git a/MicaWindowExt.cs b/MicaWindowExt.cs new file mode 100644 index 0000000..30e5b91 --- /dev/null +++ b/MicaWindowExt.cs @@ -0,0 +1,144 @@ +using Microsoft.UI; +using Microsoft.UI.Composition; +using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using Windows.Graphics; +using WinRT.Interop; + +namespace HearingProtector.Extensions; + +#pragma warning disable SYSLIB1054 + +public static class MicaWindowExt +{ + [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT lpPoint); + [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; } + + + public static void SetMicaStyle(this Window window, UIElement titleBar, SizeInt32? size = null, bool canResize = true, bool canMinMax = true) + { + window.SetSystemBackdrop(); + + window.ExtendsContentIntoTitleBar = true; + window.SetTitleBar(titleBar); + + // Устанавливаем место появления окна и начальный размер + window.CenterWindowOnCursorScreen(size, canResize, canMinMax); + + // Выводим окно на передний план + SetForegroundWindow(WindowNative.GetWindowHandle(window)); + } + + + + static void SetSystemBackdrop(this Window window) + { + // Проверяем версию Windows + if (Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent + ("Windows.Foundation.UniversalApiContract", 13)) // Версия, в которой появился MicaBackdrop + { + // Используем Mica на Windows 11 + window.SystemBackdrop = new MicaBackdrop() + { + Kind = MicaKind.Base // Или MicaKind.Alt для Mica Alt + }; + } + else + { + // Используем Acrylic на Windows 10 + //SystemBackdrop = new DesktopAcrylicBackdrop(); + window.SystemBackdrop = new Windows10AcrylicBackdrop(); + } + } + + + static void CenterWindowOnCursorScreen(this Window window, SizeInt32? size, bool canResize = true, bool canMinMax = true) + { + // Получаем AppWindow текущего окна + IntPtr hWnd = WindowNative.GetWindowHandle(window); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd); + AppWindow appWindow = AppWindow.GetFromWindowId(windowId); + + // Получаем позицию курсора + GetCursorPos(out POINT cursorPos); + + // Находим дисплей с курсором + DisplayArea displayArea = DisplayArea.GetFromPoint( + new PointInt32(cursorPos.X, cursorPos.Y), + DisplayAreaFallback.Nearest); + + if (!size.HasValue) + size = appWindow.Size; + + // Центрируем окно на этом дисплее + appWindow.MoveAndResize(new RectInt32( + displayArea.WorkArea.X + (displayArea.WorkArea.Width - size.Value.Width) / 2, + displayArea.WorkArea.Y + (displayArea.WorkArea.Height - size.Value.Height) / 2, + size.Value.Width, size.Value.Height)); + + // Запрещаем пользовательское изменение размера + if (!canResize && appWindow.Presenter is OverlappedPresenter presenter) + { + presenter.IsResizable = false; + // При желании можно также запретить сворачивание/разворачивание: + if (!canMinMax) + { + presenter.IsMaximizable = false; + presenter.IsMinimizable = false; + } + } + } +} + +// Кастомный Backdrop для Windows 10 +public partial class Windows10AcrylicBackdrop : SystemBackdrop +{ + DesktopAcrylicController? controller; + + protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot) + { + // Call the base method to initialize the default configuration object. + base.OnTargetConnected(connectedTarget, xamlRoot); + + // This example does not support sharing MicaSystemBackdrop instances. + if (controller is not null) + { + throw new Exception("This controller cannot be shared"); + } + + controller = new DesktopAcrylicController(); + // Set configuration. + SystemBackdropConfiguration defaultConfig = GetDefaultSystemBackdropConfiguration(connectedTarget, xamlRoot); + controller.SetSystemBackdropConfiguration(defaultConfig); + // Add target. + controller.AddSystemBackdropTarget(connectedTarget); + + controller.TintOpacity = 0.8f; + controller.LuminosityOpacity = 0.9f; + } + + protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget) + { + base.OnTargetDisconnected(disconnectedTarget); + + controller?.RemoveSystemBackdropTarget(disconnectedTarget); + controller = default; + } +} + +public static class ReadTextExt +{ + public static string[] FileReadAllLines(this string filePath, Encoding? enc = null) + { + using var reader = new StreamReader(filePath, enc ?? Encoding.GetEncoding(1251), detectEncodingFromByteOrderMarks: true); + string content = reader.ReadToEnd().Replace("\r", ""); + return content.Split("\n"); + } +} \ No newline at end of file diff --git a/Package.appxmanifest b/Package.appxmanifest new file mode 100644 index 0000000..494cea8 --- /dev/null +++ b/Package.appxmanifest @@ -0,0 +1,53 @@ + + + + + + + + + + HearingProtector + QWERTY + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Program.cs b/Program.cs index 0298498..2be98c7 100644 --- a/Program.cs +++ b/Program.cs @@ -1,79 +1,49 @@ -using NAudio.CoreAudioApi; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; +using Microsoft.Windows.AppLifecycle; +using System; +using System.Threading; namespace HearingProtector; -internal static class Program +public static class Program { - // ─── Настройки — только то, что интересно пользователю ─────────────── - private const double MinVolumePercent = 1.0; // до какого % убавляем - private const double FullDecayMinutes = 30.0; // за сколько минут 100% → MinVolume - private const int PollMs = 1000; // шаг цикла - private const float UserChangeEps = 0.005f;// порог «ручное изменение» - private const int LogEveryTicks = 30; // лог раз в 30 сек - - // ─── Производные величины — считаются автоматически ────────────────── - private static readonly double MinVolume = MinVolumePercent / 100.0; - private static readonly double TotalTicks = FullDecayMinutes * 60_000.0 / PollMs; - private static readonly double DecayPerTick = Math.Pow(MinVolume, 1.0 / TotalTicks); - private static readonly double DbPerTick = 20.0 * Math.Log10(DecayPerTick); - - private static async Task Main() + [STAThread] + static void Main(string[] ___) { - using var enumerator = new MMDeviceEnumerator(); - var device = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); - var volume = device.AudioEndpointVolume; + var mainInstance = AppInstance.FindOrRegisterForKey("HearingProtector_Main"); - // ── Показываем пользователю, что мы из его настроек вывели ── - Console.WriteLine($"Диапазон: 100% → {MinVolumePercent:F1}%"); - Console.WriteLine($"Заданное время: {FullDecayMinutes:F1} мин " + - $"({TotalTicks:F0} тиков по {PollMs} мс)"); - Console.WriteLine($"Множитель/тик: ×{DecayPerTick:F6} " + - $"= {DbPerTick:F4} дБ"); - Console.WriteLine(); - - float current = volume.MasterVolumeLevelScalar; - float lastSet = current; - - Console.WriteLine($"Стартуем с {current * 100:F1}%. Ctrl+C — выход."); - Console.WriteLine(); - - int tick = 0; - while (true) + if (!mainInstance.IsCurrent) { - float actual = volume.MasterVolumeLevelScalar; + // Мы не первый экземпляр — просим основной обработать активацию и выходим. + var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); + mainInstance.RedirectActivationToAsync(activationArgs).AsTask().Wait(); + return; + } - // 1) Реакция на ручное изменение громкости. - if (Math.Abs(actual - lastSet) > UserChangeEps) - { - var dir = actual > lastSet ? "повышение" : "понижение"; - Console.WriteLine( - $"[{DateTime.Now:HH:mm:ss}] Ручное изменение ({dir}): " + - $"{lastSet * 100:F1}% → {actual * 100:F1}%"); - current = actual; - } + // Подписываемся на активации от будущих попыток запуска. + mainInstance.Activated += OnInstanceActivated; - // 2) Один экспоненциальный шаг вниз. - current = (float)Math.Max(MinVolume, current * DecayPerTick); + // Стандартный запуск WinUI с правильным SynchronizationContext. + Application.Start(__ => + { + var context = new DispatcherQueueSynchronizationContext( + DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + _ = new App(); + }); + } - // 3) Применяем, если значение реально отличается. - if (Math.Abs(volume.MasterVolumeLevelScalar - current) > 0.0005f) - volume.MasterVolumeLevelScalar = current; + // Срабатывает, когда кто-то пытается запустить второй экземпляр. + private static void OnInstanceActivated(object? sender, AppActivationArguments e) + { + // ВАЖНО: событие приходит из НЕ-UI потока, поэтому нужно маршалить. + var app = App.CurrentApp; + var window = app?.MainWindow; - lastSet = current; - - // 4) Редкий лог с ETA. - if (++tick % LogEveryTicks == 0) - { - double etaSec = Math.Log(MinVolume / current) / Math.Log(DecayPerTick) - * PollMs / 1000.0; - var eta = TimeSpan.FromSeconds(Math.Max(0, etaSec)); - Console.WriteLine( - $"[{DateTime.Now:HH:mm:ss}] " + - $"Громкость: {current * 100:F1}% " + - $"(до минимума ~{eta:hh\\:mm\\:ss})"); - } - - await Task.Delay(PollMs); + if (window is MainWindow mw) + { + window.DispatcherQueue.TryEnqueue(() => mw.RestoreFromTray()); } } } \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..5fb59f2 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "HearingProtector": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/RelayCommand.cs b/RelayCommand.cs new file mode 100644 index 0000000..5412503 --- /dev/null +++ b/RelayCommand.cs @@ -0,0 +1,21 @@ +using System; +using System.Windows.Input; + +namespace HearingProtector; + +public sealed class RelayCommand : ICommand +{ + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute; + _canExecute = canExecute; + } + + public event EventHandler? CanExecuteChanged; + public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true; + public void Execute(object? parameter) => _execute(); + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); +} \ No newline at end of file diff --git a/SettingsStore.cs b/SettingsStore.cs new file mode 100644 index 0000000..c4c34d0 --- /dev/null +++ b/SettingsStore.cs @@ -0,0 +1,62 @@ +using Microsoft.Win32; + +namespace HearingProtector; + +internal sealed record AppSettings(double MinPercent, double FullDecayMinutes) +{ + public static AppSettings Default => new(1.0, 30.0); +} + +internal static class SettingsStore +{ + private const string KeyPath = @"Software\HearingProtector"; + private const string ValueMin = "MinPercent"; + private const string ValueDecay = "FullDecayMinutes"; + + public static AppSettings Load() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey(KeyPath); + if (key is null) return AppSettings.Default; + + double min = ReadDouble(key, ValueMin, AppSettings.Default.MinPercent); + double decay = ReadDouble(key, ValueDecay, AppSettings.Default.FullDecayMinutes); + + if (min < 0 || min > 50) min = AppSettings.Default.MinPercent; + if (decay < 30 || decay > 360) decay = AppSettings.Default.FullDecayMinutes; + + return new AppSettings(min, decay); + } + catch + { + return AppSettings.Default; + } + } + + public static void Save(AppSettings s) + { + try + { + using var key = Registry.CurrentUser.CreateSubKey(KeyPath, writable: true); + if (key is null) return; + + key.SetValue(ValueMin, s.MinPercent, RegistryValueKind.String); + key.SetValue(ValueDecay, s.FullDecayMinutes, RegistryValueKind.String); + key.DeleteValue("PollMs", throwOnMissingValue: false); + } + catch { } + } + + private static double ReadDouble(RegistryKey key, string name, double fallback) + { + var raw = key.GetValue(name); + return raw switch + { + string s when double.TryParse(s, System.Globalization.CultureInfo.InvariantCulture, + out var d) => d, + double d => d, + _ => fallback, + }; + } +} \ No newline at end of file diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..25d5318 --- /dev/null +++ b/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file