feat(app): 初始化自动关机工具首个版本
实现基于 WPF 的 AutoShutdown 主界面,支持关机、重启、睡眠、休眠、唤醒、锁屏和注销等电源任务。 支持指定时间和倒计时计划、执行前提醒、系统关机撤销、Windows 唤醒任务、托盘运行、自定义图标以及 OmniNotify 通知适配。 修复关闭到托盘时的运行提醒,并支持单击托盘图标打开或收起主界面。 补充 README、发布配置和 win-x64 Release 输出要求。 Release: win-x64
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
bin/
|
||||
obj/
|
||||
Screenshots/
|
||||
*.user
|
||||
*.suo
|
||||
*.log
|
||||
27
AutoShutdown.csproj
Normal file
27
AutoShutdown.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<AssemblyName>AutoShutdown</AssemblyName>
|
||||
<RootNamespace>AutoShutdown</RootNamespace>
|
||||
<Version>1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="app.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>false</SelfContained>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
282
MainWindow.xaml
Normal file
282
MainWindow.xaml
Normal file
@@ -0,0 +1,282 @@
|
||||
<Window x:Class="AutoShutdown.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AutoShutdown"
|
||||
Icon="app.ico"
|
||||
Width="980"
|
||||
Height="560"
|
||||
MinWidth="860"
|
||||
MinHeight="540"
|
||||
Background="#F6F8FC"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="14"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Window.Resources>
|
||||
<SolidColorBrush x:Key="InkBrush" Color="#111827"/>
|
||||
<SolidColorBrush x:Key="MutedBrush" Color="#64748B"/>
|
||||
<SolidColorBrush x:Key="CardBrush" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#2563EB"/>
|
||||
<SolidColorBrush x:Key="AccentSoftBrush" Color="#E2ECFF"/>
|
||||
<SolidColorBrush x:Key="BorderBrushSoft" Color="#DEE6F2"/>
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#DC2626"/>
|
||||
<SolidColorBrush x:Key="SlateButtonBrush" Color="#475569"/>
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#008060"/>
|
||||
|
||||
<Style x:Key="ModernButton" TargetType="Button">
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="MinWidth" Value="112"/>
|
||||
<Setter Property="Padding" Value="18,0"/>
|
||||
<Setter Property="Margin" Value="0,0,12,0"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="6"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Root" Property="Opacity" Value="0.88"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Root" Property="Opacity" Value="0.76"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Root" Property="Opacity" Value="0.45"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ActionChoiceButton" TargetType="Button">
|
||||
<Setter Property="Height" Value="42"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
<Setter Property="Padding" Value="18,0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BorderBrushSoft}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="FontSize" Value="15"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="6"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Root" Property="Background" Value="#F4F8FF"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Root" Property="Background" Value="{StaticResource AccentSoftBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ModernTextBox" TargetType="TextBox">
|
||||
<Setter Property="Height" Value="30"/>
|
||||
<Setter Property="Width" Value="64"/>
|
||||
<Setter Property="Padding" Value="8,3"/>
|
||||
<Setter Property="Margin" Value="0,0,8,0"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="#CAD5E4"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Background" Value="White"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border x:Name="Root"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="5">
|
||||
<ScrollViewer x:Name="PART_ContentHost"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||
<Setter TargetName="Root" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Root" Property="Opacity" Value="0.55"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Margin" Value="0,0,0,10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid Width="18" Height="18" Margin="0,1,8,0">
|
||||
<Ellipse Stroke="#6B7280" StrokeThickness="1.4" Fill="White"/>
|
||||
<Ellipse x:Name="Dot" Width="8" Height="8" Fill="{StaticResource AccentBrush}" Opacity="0"/>
|
||||
</Grid>
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Dot" Property="Opacity" Value="1"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.55"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{StaticResource InkBrush}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Margin" Value="0,0,18,0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="Box" Width="18" Height="18" Margin="0,1,8,0"
|
||||
BorderBrush="#6B7280" BorderThickness="1.4" CornerRadius="4" Background="White">
|
||||
<Path x:Name="Tick" Data="M 4 9 L 8 13 L 15 5"
|
||||
Stroke="White" StrokeThickness="2" StrokeStartLineCap="Round"
|
||||
StrokeEndLineCap="Round" Opacity="0"/>
|
||||
</Border>
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Box" Property="Background" Value="{StaticResource AccentBrush}"/>
|
||||
<Setter TargetName="Box" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
|
||||
<Setter TargetName="Tick" Property="Opacity" Value="1"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Background="#F6F8FC">
|
||||
<Grid Margin="18" Background="#F6F8FC">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="480"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Width="920" Height="480" HorizontalAlignment="Center" VerticalAlignment="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300"/>
|
||||
<ColumnDefinition Width="16"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Background="White" CornerRadius="8" Padding="20,18">
|
||||
<StackPanel>
|
||||
<TextBlock Text="选择动作"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource InkBrush}"
|
||||
Margin="0,0,0,14"/>
|
||||
<Button x:Name="ShutdownButton" Content="关机" Style="{StaticResource ActionChoiceButton}" Click="ActionButton_Click"/>
|
||||
<Button x:Name="RestartButton" Content="重启" Style="{StaticResource ActionChoiceButton}" Click="ActionButton_Click"/>
|
||||
<Button x:Name="SleepButton" Content="睡眠" Style="{StaticResource ActionChoiceButton}" Click="ActionButton_Click"/>
|
||||
<Button x:Name="HibernateButton" Content="休眠" Style="{StaticResource ActionChoiceButton}" Click="ActionButton_Click"/>
|
||||
<Button x:Name="WakeButton" Content="唤醒" Style="{StaticResource ActionChoiceButton}" Click="ActionButton_Click"/>
|
||||
<Button x:Name="LockButton" Content="锁屏" Style="{StaticResource ActionChoiceButton}" Click="ActionButton_Click"/>
|
||||
<Button x:Name="LogOffButton" Content="注销" Style="{StaticResource ActionChoiceButton}" Click="ActionButton_Click"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2" Background="White" CornerRadius="8" Padding="28,18">
|
||||
<StackPanel>
|
||||
<TextBlock Text="任务计划"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource InkBrush}"/>
|
||||
<TextBlock x:Name="ActionTitleText"
|
||||
Margin="0,10,0,0"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource InkBrush}"/>
|
||||
<TextBlock x:Name="ActionDescriptionText"
|
||||
Margin="2,0,0,20"
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource MutedBrush}"/>
|
||||
|
||||
<RadioButton x:Name="ClockRadio" Content="指定时间" IsChecked="True" Checked="ScheduleModeChanged"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="20,0,0,18">
|
||||
<TextBox x:Name="DateInput" Style="{StaticResource ModernTextBox}" Width="120"/>
|
||||
<TextBox x:Name="TimeInput" Style="{StaticResource ModernTextBox}" Width="74"/>
|
||||
</StackPanel>
|
||||
|
||||
<RadioButton x:Name="CountdownRadio" Content="倒计时" Checked="ScheduleModeChanged"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="20,0,0,18">
|
||||
<TextBox x:Name="HoursInput" Style="{StaticResource ModernTextBox}" Text="0"/>
|
||||
<TextBlock Text="小时" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" Margin="0,0,14,0"/>
|
||||
<TextBox x:Name="MinutesInput" Style="{StaticResource ModernTextBox}" Text="30"/>
|
||||
<TextBlock Text="分钟" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,24">
|
||||
<CheckBox x:Name="ForceCheck" Content="强制关闭"/>
|
||||
<TextBlock Text="提醒" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBox x:Name="ReminderInput" Style="{StaticResource ModernTextBox}" Text="5"/>
|
||||
<TextBlock Text="分钟前" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button x:Name="StartPlanButton" Content="开始计划" Background="{StaticResource AccentBrush}" Style="{StaticResource ModernButton}" Click="StartButton_Click"/>
|
||||
<Button x:Name="CancelPlanButton" Content="取消计划" Background="{StaticResource SlateButtonBrush}" Style="{StaticResource ModernButton}" Click="CancelButton_Click"/>
|
||||
<Button x:Name="AbortShutdownButton" Content="撤销关机" Background="{StaticResource DangerBrush}" Style="{StaticResource ModernButton}" Click="AbortButton_Click"/>
|
||||
<Button Content="通知设置" Background="{StaticResource SlateButtonBrush}" Style="{StaticResource ModernButton}" Click="SettingsButton_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Background="#F8FAFC" CornerRadius="6" Padding="12,8" Margin="0,22,0,0">
|
||||
<Grid>
|
||||
<TextBlock x:Name="StatusText"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}"/>
|
||||
<TextBlock x:Name="CountdownText"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="Bold"
|
||||
Foreground="{StaticResource SuccessBrush}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
471
MainWindow.xaml.cs
Normal file
471
MainWindow.xaml.cs
Normal file
@@ -0,0 +1,471 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using AutoShutdown.Models;
|
||||
using AutoShutdown.Services;
|
||||
using Forms = System.Windows.Forms;
|
||||
|
||||
namespace AutoShutdown;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly PowerService _powerService = new();
|
||||
private readonly WakeService _wakeService = new();
|
||||
private readonly NotificationSettingsStore _settingsStore = new();
|
||||
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
private readonly Dictionary<PowerAction, System.Windows.Controls.Button> _actionButtons;
|
||||
private readonly Forms.NotifyIcon? _trayIcon;
|
||||
private readonly bool _enableTray;
|
||||
|
||||
private NotificationSettings _notificationSettings;
|
||||
private NotificationService _notificationService;
|
||||
private PowerAction _selectedAction = PowerAction.Shutdown;
|
||||
private PowerAction? _scheduledAction;
|
||||
private DateTime? _scheduledAt;
|
||||
private bool _hasExecuted;
|
||||
private bool _reminderShown;
|
||||
private bool _allowExit;
|
||||
|
||||
public MainWindow(bool enableTray = true)
|
||||
{
|
||||
_enableTray = enableTray;
|
||||
_notificationSettings = _settingsStore.Load();
|
||||
_notificationService = new NotificationService(_notificationSettings);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
_actionButtons = new Dictionary<PowerAction, System.Windows.Controls.Button>
|
||||
{
|
||||
[PowerAction.Shutdown] = ShutdownButton,
|
||||
[PowerAction.Restart] = RestartButton,
|
||||
[PowerAction.Sleep] = SleepButton,
|
||||
[PowerAction.Hibernate] = HibernateButton,
|
||||
[PowerAction.Wake] = WakeButton,
|
||||
[PowerAction.Lock] = LockButton,
|
||||
[PowerAction.LogOff] = LogOffButton
|
||||
};
|
||||
|
||||
foreach (var (action, button) in _actionButtons)
|
||||
{
|
||||
button.Tag = action;
|
||||
button.ToolTip = action.Description();
|
||||
}
|
||||
|
||||
DateInput.Text = DateTime.Today.ToString("yyyy/M/d");
|
||||
TimeInput.Text = DateTime.Now.AddMinutes(30).ToString("HH:mm");
|
||||
ForceCheck.ToolTip = "强制关闭未响应程序,仅用于关机、重启、注销等动作。";
|
||||
|
||||
_trayIcon = _enableTray ? CreateTrayIcon() : null;
|
||||
|
||||
SelectAction(PowerAction.Shutdown);
|
||||
UpdateScheduleInputs();
|
||||
UpdateStatus();
|
||||
|
||||
_timer.Tick += (_, _) => OnTick();
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
if (_enableTray && !_allowExit)
|
||||
{
|
||||
e.Cancel = true;
|
||||
HideToTray();
|
||||
return;
|
||||
}
|
||||
|
||||
_trayIcon?.Dispose();
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
private Forms.NotifyIcon CreateTrayIcon()
|
||||
{
|
||||
var menu = new Forms.ContextMenuStrip();
|
||||
menu.Items.Add("显示/收起主窗口", null, (_, _) => Dispatcher.Invoke(ToggleMainWindowFromTray));
|
||||
menu.Items.Add("取消当前计划", null, async (_, _) => await Dispatcher.InvokeAsync(CancelCurrentPlanAsync));
|
||||
menu.Items.Add("OmniNotify 适配设置", null, (_, _) => Dispatcher.Invoke(OpenNotificationSettings));
|
||||
menu.Items.Add(new Forms.ToolStripSeparator());
|
||||
menu.Items.Add("退出 AutoShutdown", null, (_, _) => ExitFromTray());
|
||||
|
||||
var icon = new Forms.NotifyIcon
|
||||
{
|
||||
Icon = LoadTrayIcon(),
|
||||
Text = "AutoShutdown",
|
||||
ContextMenuStrip = menu,
|
||||
Visible = true
|
||||
};
|
||||
icon.MouseClick += (_, e) =>
|
||||
{
|
||||
if (e.Button == Forms.MouseButtons.Left)
|
||||
{
|
||||
Dispatcher.Invoke(ToggleMainWindowFromTray);
|
||||
}
|
||||
};
|
||||
return icon;
|
||||
}
|
||||
|
||||
private static System.Drawing.Icon LoadTrayIcon()
|
||||
{
|
||||
var processPath = Environment.ProcessPath;
|
||||
return !string.IsNullOrWhiteSpace(processPath) && System.Drawing.Icon.ExtractAssociatedIcon(processPath) is { } icon
|
||||
? icon
|
||||
: System.Drawing.SystemIcons.Application;
|
||||
}
|
||||
|
||||
private void HideToTray(bool showHint = true)
|
||||
{
|
||||
Hide();
|
||||
if (!showHint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_notificationSettings.Mode == NotificationMode.OmniNotify)
|
||||
{
|
||||
_ = _notificationService.NotifyAsync("AutoShutdown 已在托盘运行", "关闭窗口不会退出应用,可在托盘菜单中退出。");
|
||||
return;
|
||||
}
|
||||
|
||||
_trayIcon?.ShowBalloonTip(2500, "AutoShutdown 已在托盘运行", "关闭窗口不会退出应用,可在托盘菜单中退出。", Forms.ToolTipIcon.Info);
|
||||
}
|
||||
|
||||
private void ToggleMainWindowFromTray()
|
||||
{
|
||||
if (IsVisible && WindowState != WindowState.Minimized)
|
||||
{
|
||||
HideToTray(showHint: false);
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreFromTray();
|
||||
}
|
||||
|
||||
private void RestoreFromTray()
|
||||
{
|
||||
Show();
|
||||
WindowState = WindowState.Normal;
|
||||
Activate();
|
||||
}
|
||||
|
||||
private void ExitFromTray()
|
||||
{
|
||||
_allowExit = true;
|
||||
Dispatcher.Invoke(Close);
|
||||
}
|
||||
|
||||
private void UpdateTrayText(string text)
|
||||
{
|
||||
if (_trayIcon is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_trayIcon.Text = text.Length > 63 ? text[..63] : text;
|
||||
}
|
||||
|
||||
private void ActionButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is System.Windows.Controls.Button { Tag: PowerAction action })
|
||||
{
|
||||
SelectAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleModeChanged(object sender, RoutedEventArgs e) => UpdateScheduleInputs();
|
||||
|
||||
private async void StartButton_Click(object sender, RoutedEventArgs e) => await StartPlanAsync();
|
||||
|
||||
private async void CancelButton_Click(object sender, RoutedEventArgs e) => await CancelPlanAsync();
|
||||
|
||||
private async void AbortButton_Click(object sender, RoutedEventArgs e) => await AbortSystemShutdownAsync();
|
||||
|
||||
private void SettingsButton_Click(object sender, RoutedEventArgs e) => OpenNotificationSettings();
|
||||
|
||||
private void OpenNotificationSettings()
|
||||
{
|
||||
var window = new NotificationSettingsWindow(_notificationSettings, _notificationService)
|
||||
{
|
||||
Owner = this
|
||||
};
|
||||
|
||||
if (window.ShowDialog() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_notificationSettings = window.Settings;
|
||||
_settingsStore.Save(_notificationSettings);
|
||||
_notificationService.UpdateSettings(_notificationSettings);
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private void SelectAction(PowerAction action)
|
||||
{
|
||||
_selectedAction = action;
|
||||
ActionTitleText.Text = action.Label();
|
||||
ActionDescriptionText.Text = action.Description();
|
||||
|
||||
foreach (var (item, button) in _actionButtons)
|
||||
{
|
||||
var selected = item == action;
|
||||
button.Background = selected ? BrushFrom("#E2ECFF") : BrushFrom("#FFFFFF");
|
||||
button.Foreground = selected ? BrushFrom("#2563EB") : BrushFrom("#111827");
|
||||
button.BorderBrush = selected ? BrushFrom("#2563EB") : BrushFrom("#DEE6F2");
|
||||
button.BorderThickness = selected ? new Thickness(2) : new Thickness(1);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartPlanAsync()
|
||||
{
|
||||
if (!TryResolveScheduleTime(out var scheduledAt))
|
||||
{
|
||||
await _notificationService.NotifyAsync(this, "计划无效", "请检查时间或倒计时输入。", MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scheduledAt <= DateTime.Now.AddSeconds(1))
|
||||
{
|
||||
await _notificationService.NotifyAsync(this, "计划无效", "请选择一个晚于当前时间的计划。", MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var scheduledAction = _selectedAction;
|
||||
var forceApps = ForceCheck.IsChecked == true;
|
||||
_scheduledAt = scheduledAt;
|
||||
_scheduledAction = scheduledAction;
|
||||
_hasExecuted = false;
|
||||
_reminderShown = false;
|
||||
UpdateStatus();
|
||||
await Dispatcher.Yield(DispatcherPriority.Background);
|
||||
|
||||
var seconds = (int)Math.Ceiling((scheduledAt - DateTime.Now).TotalSeconds);
|
||||
if (scheduledAction == PowerAction.Wake)
|
||||
{
|
||||
await TryRunAsync(
|
||||
() => _wakeService.ScheduleWake(scheduledAt),
|
||||
"计划已启动",
|
||||
"已创建 Windows 唤醒任务。请确保电源选项允许唤醒计时器。");
|
||||
}
|
||||
else if (scheduledAction is PowerAction.Shutdown or PowerAction.Restart)
|
||||
{
|
||||
await TryRunAsync(
|
||||
() => _powerService.Execute(scheduledAction, seconds, forceApps),
|
||||
"计划已启动",
|
||||
"已交给 Windows 系统计划执行。");
|
||||
}
|
||||
else
|
||||
{
|
||||
await _notificationService.NotifyAsync(this, "计划已启动", $"{scheduledAction.Label()} 将在 {scheduledAt:yyyy-MM-dd HH:mm} 执行。");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelPlanAsync()
|
||||
{
|
||||
var hadPlan = _scheduledAt is not null;
|
||||
var scheduledAction = _scheduledAction;
|
||||
_scheduledAt = null;
|
||||
_scheduledAction = null;
|
||||
_hasExecuted = false;
|
||||
_reminderShown = false;
|
||||
UpdateStatus();
|
||||
|
||||
if (scheduledAction == PowerAction.Wake)
|
||||
{
|
||||
await TryCancelWakeAsync();
|
||||
}
|
||||
|
||||
if (hadPlan && scheduledAction is not null)
|
||||
{
|
||||
await _notificationService.NotifyAsync(this, "计划已取消", $"已取消 {scheduledAction.Value.Label()} 计划。");
|
||||
}
|
||||
}
|
||||
|
||||
private Task CancelCurrentPlanAsync()
|
||||
{
|
||||
return _scheduledAction is PowerAction.Shutdown or PowerAction.Restart
|
||||
? AbortSystemShutdownAsync()
|
||||
: CancelPlanAsync();
|
||||
}
|
||||
|
||||
private async Task AbortSystemShutdownAsync()
|
||||
{
|
||||
if (_powerService.CancelShutdown())
|
||||
{
|
||||
_scheduledAt = null;
|
||||
_scheduledAction = null;
|
||||
_hasExecuted = false;
|
||||
_reminderShown = false;
|
||||
UpdateStatus();
|
||||
await _notificationService.NotifyAsync(this, "已撤销", "已请求 Windows 撤销待执行的关机/重启任务。");
|
||||
return;
|
||||
}
|
||||
|
||||
await _notificationService.NotifyAsync(this, "无需撤销", "当前没有可撤销的系统关机/重启任务,或系统拒绝了该请求。");
|
||||
}
|
||||
|
||||
private async void OnTick()
|
||||
{
|
||||
if (_scheduledAt is null)
|
||||
{
|
||||
UpdateStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
var remaining = _scheduledAt.Value - DateTime.Now;
|
||||
if (remaining.TotalSeconds <= 0 && !_hasExecuted)
|
||||
{
|
||||
_hasExecuted = true;
|
||||
var scheduledAction = _scheduledAction ?? _selectedAction;
|
||||
var forceApps = ForceCheck.IsChecked == true;
|
||||
if (scheduledAction is not (PowerAction.Shutdown or PowerAction.Restart or PowerAction.Wake))
|
||||
{
|
||||
await TryRunAsync(
|
||||
() => _powerService.Execute(scheduledAction, 0, forceApps),
|
||||
"执行完成",
|
||||
$"{scheduledAction.Label()} 已执行。",
|
||||
showSuccess: false);
|
||||
}
|
||||
|
||||
_scheduledAt = null;
|
||||
_scheduledAction = null;
|
||||
}
|
||||
else if (!_reminderShown && TryReadInt(ReminderInput.Text, 0, 120, out var reminder) && reminder > 0 && remaining.TotalMinutes <= reminder)
|
||||
{
|
||||
_reminderShown = true;
|
||||
var scheduledAction = _scheduledAction ?? _selectedAction;
|
||||
await _notificationService.NotifyAsync(this, "任务提醒", $"{scheduledAction.Label()} 将在 {FormatRemaining(remaining)} 后执行。");
|
||||
}
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private bool TryResolveScheduleTime(out DateTime scheduledAt)
|
||||
{
|
||||
if (ClockRadio.IsChecked == true)
|
||||
{
|
||||
if (!DateTime.TryParse(DateInput.Text, out var date) || !TimeSpan.TryParse(TimeInput.Text, out var time))
|
||||
{
|
||||
scheduledAt = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduledAt = date.Date.Add(time);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadInt(HoursInput.Text, 0, 168, out var hours) || !TryReadInt(MinutesInput.Text, 0, 59, out var minutes))
|
||||
{
|
||||
scheduledAt = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduledAt = DateTime.Now.AddHours(hours).AddMinutes(minutes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateScheduleInputs()
|
||||
{
|
||||
if (DateInput is null || TimeInput is null || HoursInput is null || MinutesInput is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var byClock = ClockRadio.IsChecked == true;
|
||||
DateInput.IsEnabled = byClock;
|
||||
TimeInput.IsEnabled = byClock;
|
||||
HoursInput.IsEnabled = !byClock;
|
||||
MinutesInput.IsEnabled = !byClock;
|
||||
}
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
UpdatePlanActionButtons();
|
||||
|
||||
if (_scheduledAt is null)
|
||||
{
|
||||
StatusText.Text = _notificationSettings.Mode == NotificationMode.OmniNotify
|
||||
? $"当前没有运行中的计划。提示:OmniNotify / {_notificationSettings.Channel}"
|
||||
: "当前没有运行中的计划。";
|
||||
CountdownText.Text = "待命";
|
||||
UpdateTrayText("AutoShutdown - 待命");
|
||||
return;
|
||||
}
|
||||
|
||||
var remaining = _scheduledAt.Value - DateTime.Now;
|
||||
if (remaining < TimeSpan.Zero)
|
||||
{
|
||||
remaining = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
var scheduledAction = _scheduledAction ?? _selectedAction;
|
||||
StatusText.Text = $"下一次任务:{_scheduledAt:yyyy-MM-dd HH:mm} 执行 {scheduledAction.Label()}";
|
||||
CountdownText.Text = FormatRemaining(remaining);
|
||||
UpdateTrayText($"AutoShutdown - {scheduledAction.Label()} {FormatRemaining(remaining)}");
|
||||
}
|
||||
|
||||
private void UpdatePlanActionButtons()
|
||||
{
|
||||
if (StartPlanButton is null || CancelPlanButton is null || AbortShutdownButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hasPlan = _scheduledAt is not null;
|
||||
var needsSystemAbort = hasPlan && _scheduledAction is PowerAction.Shutdown or PowerAction.Restart;
|
||||
|
||||
StartPlanButton.Visibility = hasPlan ? Visibility.Collapsed : Visibility.Visible;
|
||||
CancelPlanButton.Visibility = hasPlan && !needsSystemAbort ? Visibility.Visible : Visibility.Collapsed;
|
||||
AbortShutdownButton.Visibility = needsSystemAbort ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async Task TryRunAsync(Action action, string title, string successMessage, bool showSuccess = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Run(action);
|
||||
if (showSuccess)
|
||||
{
|
||||
await _notificationService.NotifyAsync(this, title, successMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_scheduledAt = null;
|
||||
_scheduledAction = null;
|
||||
UpdateStatus();
|
||||
await _notificationService.NotifyAsync(this, "启动失败", ex.Message, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryCancelWakeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Run(_wakeService.CancelWake);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 取消普通计划时不因为系统任务不存在或权限差异打断用户。
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadInt(string text, int min, int max, out int value)
|
||||
{
|
||||
return int.TryParse(text, out value) && value >= min && value <= max;
|
||||
}
|
||||
|
||||
private static string FormatRemaining(TimeSpan value)
|
||||
{
|
||||
if (value.TotalDays >= 1)
|
||||
{
|
||||
return $"{(int)value.TotalDays}天 {value.Hours:D2}:{value.Minutes:D2}:{value.Seconds:D2}";
|
||||
}
|
||||
|
||||
return $"{Math.Max(0, (int)value.TotalHours):D2}:{value.Minutes:D2}:{value.Seconds:D2}";
|
||||
}
|
||||
|
||||
private static System.Windows.Media.Brush BrushFrom(string hex)
|
||||
{
|
||||
return (System.Windows.Media.Brush)new System.Windows.Media.BrushConverter().ConvertFromString(hex)!;
|
||||
}
|
||||
}
|
||||
7
Models/NotificationMode.cs
Normal file
7
Models/NotificationMode.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace AutoShutdown.Models;
|
||||
|
||||
internal enum NotificationMode
|
||||
{
|
||||
Local,
|
||||
OmniNotify
|
||||
}
|
||||
12
Models/NotificationSettings.cs
Normal file
12
Models/NotificationSettings.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace AutoShutdown.Models;
|
||||
|
||||
internal sealed class NotificationSettings
|
||||
{
|
||||
public NotificationMode Mode { get; set; } = NotificationMode.Local;
|
||||
|
||||
public string OmniNotifyEndpoint { get; set; } = "http://127.0.0.1:19845/notify";
|
||||
|
||||
public string Channel { get; set; } = "default";
|
||||
|
||||
public static NotificationSettings Default() => new();
|
||||
}
|
||||
39
Models/PowerAction.cs
Normal file
39
Models/PowerAction.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace AutoShutdown.Models;
|
||||
|
||||
internal enum PowerAction
|
||||
{
|
||||
Shutdown,
|
||||
Restart,
|
||||
Sleep,
|
||||
Hibernate,
|
||||
Wake,
|
||||
Lock,
|
||||
LogOff
|
||||
}
|
||||
|
||||
internal static class PowerActionText
|
||||
{
|
||||
public static string Label(this PowerAction action) => action switch
|
||||
{
|
||||
PowerAction.Shutdown => "关机",
|
||||
PowerAction.Restart => "重启",
|
||||
PowerAction.Sleep => "睡眠",
|
||||
PowerAction.Hibernate => "休眠",
|
||||
PowerAction.Wake => "唤醒",
|
||||
PowerAction.Lock => "锁屏",
|
||||
PowerAction.LogOff => "注销",
|
||||
_ => action.ToString()
|
||||
};
|
||||
|
||||
public static string Description(this PowerAction action) => action switch
|
||||
{
|
||||
PowerAction.Shutdown => "关闭电脑",
|
||||
PowerAction.Restart => "重新启动电脑",
|
||||
PowerAction.Sleep => "保留会话并进入低功耗状态",
|
||||
PowerAction.Hibernate => "保存当前状态后断电待机",
|
||||
PowerAction.Wake => "通过任务计划从睡眠或休眠中唤醒",
|
||||
PowerAction.Lock => "锁定当前 Windows 会话",
|
||||
PowerAction.LogOff => "注销当前用户",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
220
NotificationSettingsWindow.cs
Normal file
220
NotificationSettingsWindow.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using AutoShutdown.Models;
|
||||
using AutoShutdown.Services;
|
||||
using WpfBrush = System.Windows.Media.Brush;
|
||||
using WpfBrushes = System.Windows.Media.Brushes;
|
||||
using WpfButton = System.Windows.Controls.Button;
|
||||
using WpfCursors = System.Windows.Input.Cursors;
|
||||
using WpfFontFamily = System.Windows.Media.FontFamily;
|
||||
using WpfOrientation = System.Windows.Controls.Orientation;
|
||||
using WpfRadioButton = System.Windows.Controls.RadioButton;
|
||||
using WpfTextBox = System.Windows.Controls.TextBox;
|
||||
|
||||
namespace AutoShutdown;
|
||||
|
||||
internal sealed class NotificationSettingsWindow : Window
|
||||
{
|
||||
private readonly NotificationService _notificationService;
|
||||
private readonly WpfRadioButton _localMode = new() { Content = "软件本身" };
|
||||
private readonly WpfRadioButton _omniNotifyMode = new() { Content = "OmniNotify" };
|
||||
private readonly WpfTextBox _endpoint = new();
|
||||
private readonly WpfTextBox _channel = new();
|
||||
private readonly TextBlock _status = new();
|
||||
|
||||
public NotificationSettings Settings { get; private set; }
|
||||
|
||||
public NotificationSettingsWindow(NotificationSettings settings, NotificationService notificationService)
|
||||
{
|
||||
_notificationService = notificationService;
|
||||
Settings = new NotificationSettings
|
||||
{
|
||||
Mode = settings.Mode,
|
||||
OmniNotifyEndpoint = settings.OmniNotifyEndpoint,
|
||||
Channel = settings.Channel
|
||||
};
|
||||
|
||||
Title = "OmniNotify 适配设置";
|
||||
Width = 640;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
MinWidth = 640;
|
||||
MaxWidth = 640;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
Background = BrushFrom("#F6F8FC");
|
||||
FontFamily = new WpfFontFamily("Microsoft YaHei UI");
|
||||
FontSize = 14;
|
||||
|
||||
Content = BuildContent();
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
private UIElement BuildContent()
|
||||
{
|
||||
var host = new Grid
|
||||
{
|
||||
Background = BrushFrom("#F6F8FC")
|
||||
};
|
||||
|
||||
var root = new Border
|
||||
{
|
||||
Background = BrushFrom("#FFFFFF"),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(26),
|
||||
Margin = new Thickness(22)
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
root.Child = stack;
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "通知方式",
|
||||
FontSize = 22,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = BrushFrom("#111827")
|
||||
});
|
||||
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "选择提示由 AutoShutdown 弹窗显示,或发送给本机 OmniNotify。",
|
||||
Margin = new Thickness(0, 4, 0, 20),
|
||||
Foreground = BrushFrom("#64748B"),
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
|
||||
stack.Children.Add(MakeLabel("提示模式"));
|
||||
var modeRow = new StackPanel { Orientation = WpfOrientation.Horizontal, Margin = new Thickness(0, 8, 0, 18) };
|
||||
StyleRadio(_localMode);
|
||||
StyleRadio(_omniNotifyMode);
|
||||
_localMode.Margin = new Thickness(0, 0, 22, 0);
|
||||
modeRow.Children.Add(_localMode);
|
||||
modeRow.Children.Add(_omniNotifyMode);
|
||||
stack.Children.Add(modeRow);
|
||||
|
||||
stack.Children.Add(MakeLabel("监听地址"));
|
||||
StyleInput(_endpoint, 536);
|
||||
_endpoint.Margin = new Thickness(0, 8, 0, 14);
|
||||
stack.Children.Add(_endpoint);
|
||||
|
||||
stack.Children.Add(MakeLabel("频道名"));
|
||||
StyleInput(_channel, 260);
|
||||
_channel.Margin = new Thickness(0, 8, 0, 12);
|
||||
stack.Children.Add(_channel);
|
||||
|
||||
_status.Text = "OmniNotify 模式需要先在 OmniNotify 中创建对应频道。";
|
||||
_status.Foreground = BrushFrom("#64748B");
|
||||
_status.TextWrapping = TextWrapping.Wrap;
|
||||
_status.Margin = new Thickness(0, 0, 0, 20);
|
||||
stack.Children.Add(_status);
|
||||
|
||||
var buttons = new StackPanel
|
||||
{
|
||||
Orientation = WpfOrientation.Horizontal,
|
||||
HorizontalAlignment = System.Windows.HorizontalAlignment.Right
|
||||
};
|
||||
var test = MakeButton("测试发送", BrushFrom("#475569"));
|
||||
test.Click += async (_, _) => await TestAsync();
|
||||
var cancel = MakeButton("取消", BrushFrom("#64748B"));
|
||||
cancel.Click += (_, _) => DialogResult = false;
|
||||
var save = MakeButton("保存", BrushFrom("#2563EB"));
|
||||
save.Click += (_, _) => SaveAndClose();
|
||||
buttons.Children.Add(test);
|
||||
buttons.Children.Add(cancel);
|
||||
buttons.Children.Add(save);
|
||||
stack.Children.Add(buttons);
|
||||
|
||||
host.Children.Add(root);
|
||||
return host;
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
_localMode.IsChecked = Settings.Mode == NotificationMode.Local;
|
||||
_omniNotifyMode.IsChecked = Settings.Mode == NotificationMode.OmniNotify;
|
||||
_endpoint.Text = Settings.OmniNotifyEndpoint;
|
||||
_channel.Text = Settings.Channel;
|
||||
}
|
||||
|
||||
private async Task TestAsync()
|
||||
{
|
||||
var testSettings = ReadSettings();
|
||||
_status.Text = "正在发送测试消息...";
|
||||
_status.Foreground = BrushFrom("#64748B");
|
||||
var ok = await _notificationService.TestOmniNotifyAsync(testSettings);
|
||||
_status.Text = ok
|
||||
? "测试消息已发送。"
|
||||
: "测试失败,请确认 OmniNotify 正在运行、监听地址正确,并且频道已创建。";
|
||||
_status.Foreground = ok ? BrushFrom("#008060") : BrushFrom("#DC2626");
|
||||
}
|
||||
|
||||
private void SaveAndClose()
|
||||
{
|
||||
Settings = ReadSettings();
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private NotificationSettings ReadSettings()
|
||||
{
|
||||
return new NotificationSettings
|
||||
{
|
||||
Mode = _omniNotifyMode.IsChecked == true ? NotificationMode.OmniNotify : NotificationMode.Local,
|
||||
OmniNotifyEndpoint = string.IsNullOrWhiteSpace(_endpoint.Text) ? "http://127.0.0.1:19845/notify" : _endpoint.Text.Trim(),
|
||||
Channel = string.IsNullOrWhiteSpace(_channel.Text) ? "default" : _channel.Text.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
private static TextBlock MakeLabel(string text)
|
||||
{
|
||||
return new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = BrushFrom("#111827")
|
||||
};
|
||||
}
|
||||
|
||||
private static void StyleRadio(WpfRadioButton radio)
|
||||
{
|
||||
radio.Foreground = BrushFrom("#111827");
|
||||
radio.FontSize = 14;
|
||||
radio.Cursor = WpfCursors.Hand;
|
||||
}
|
||||
|
||||
private static void StyleInput(WpfTextBox input, double width)
|
||||
{
|
||||
input.Height = 34;
|
||||
input.Width = width;
|
||||
input.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
|
||||
input.Padding = new Thickness(10, 5, 10, 5);
|
||||
input.BorderBrush = BrushFrom("#CBD5E1");
|
||||
input.BorderThickness = new Thickness(1);
|
||||
input.Background = WpfBrushes.White;
|
||||
input.Foreground = BrushFrom("#111827");
|
||||
}
|
||||
|
||||
private static WpfButton MakeButton(string text, WpfBrush background)
|
||||
{
|
||||
var button = new WpfButton
|
||||
{
|
||||
Content = text,
|
||||
Height = 38,
|
||||
MinWidth = 92,
|
||||
Margin = new Thickness(10, 0, 0, 0),
|
||||
Padding = new Thickness(16, 0, 16, 0),
|
||||
Background = background,
|
||||
Foreground = WpfBrushes.White,
|
||||
BorderThickness = new Thickness(0),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Cursor = WpfCursors.Hand
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
private static WpfBrush BrushFrom(string hex)
|
||||
{
|
||||
return (WpfBrush)new BrushConverter().ConvertFromString(hex)!;
|
||||
}
|
||||
}
|
||||
103
Program.cs
Normal file
103
Program.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace AutoShutdown;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var app = new System.Windows.Application();
|
||||
|
||||
#if DEBUG
|
||||
if (args.Any(arg => arg.Equals("--capture-notification-settings-ui", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
CaptureNotificationSettingsUiAndExit(app, args);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Any(arg => arg.Equals("--capture-ui", StringComparison.OrdinalIgnoreCase) || arg.StartsWith("--capture-ui=", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
CaptureUiAndExit(app, args);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
app.Run(new MainWindow());
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static void CaptureUiAndExit(System.Windows.Application app, string[] args)
|
||||
{
|
||||
var size = ResolveCaptureSize(args);
|
||||
var window = new MainWindow(enableTray: false)
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
Left = -20000,
|
||||
Top = -20000,
|
||||
Width = size.Width,
|
||||
Height = size.Height,
|
||||
ShowInTaskbar = false
|
||||
};
|
||||
|
||||
window.ContentRendered += (_, _) =>
|
||||
{
|
||||
window.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var requestedPath = args
|
||||
.FirstOrDefault(arg => arg.StartsWith("--capture-ui=", StringComparison.OrdinalIgnoreCase))
|
||||
?.Split('=', 2)[1];
|
||||
|
||||
var path = Services.ScreenshotService.Capture(window, requestedPath);
|
||||
Console.WriteLine(path);
|
||||
window.Close();
|
||||
}, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||
};
|
||||
|
||||
app.Run(window);
|
||||
}
|
||||
|
||||
private static void CaptureNotificationSettingsUiAndExit(System.Windows.Application app, string[] args)
|
||||
{
|
||||
var window = new NotificationSettingsWindow(
|
||||
Models.NotificationSettings.Default(),
|
||||
new Services.NotificationService(Models.NotificationSettings.Default()))
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual,
|
||||
Left = -20000,
|
||||
Top = -20000,
|
||||
ShowInTaskbar = false
|
||||
};
|
||||
|
||||
window.ContentRendered += (_, _) =>
|
||||
{
|
||||
window.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var requestedPath = args
|
||||
.FirstOrDefault(arg => arg.StartsWith("--capture-ui=", StringComparison.OrdinalIgnoreCase))
|
||||
?.Split('=', 2)[1];
|
||||
|
||||
var path = Services.ScreenshotService.Capture(window, requestedPath);
|
||||
Console.WriteLine(path);
|
||||
window.Close();
|
||||
}, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||
};
|
||||
|
||||
app.Run(window);
|
||||
}
|
||||
|
||||
private static System.Windows.Size ResolveCaptureSize(string[] args)
|
||||
{
|
||||
var sizeArg = args.FirstOrDefault(arg => arg.StartsWith("--capture-size=", StringComparison.OrdinalIgnoreCase));
|
||||
if (sizeArg is null)
|
||||
{
|
||||
return new System.Windows.Size(1100, 760);
|
||||
}
|
||||
|
||||
var raw = sizeArg.Split('=', 2)[1].ToLowerInvariant().Split('x', 2);
|
||||
return raw.Length == 2 && double.TryParse(raw[0], out var width) && double.TryParse(raw[1], out var height)
|
||||
? new System.Windows.Size(width, height)
|
||||
: new System.Windows.Size(1100, 760);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
56
README.md
Normal file
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# AutoShutdown
|
||||
|
||||
面向 Windows 10 及以上系统的自动电源任务工具,使用 C# WPF 和 .NET 8 开发,无第三方依赖。
|
||||
|
||||
## 功能
|
||||
|
||||
- 按指定日期时间或倒计时执行任务
|
||||
- 支持关机、重启、睡眠、休眠、唤醒、锁屏、注销
|
||||
- 关机/重启交给 Windows 系统计划执行,关闭应用后仍可生效
|
||||
- 唤醒通过 Windows 任务计划程序实现,可从睡眠/休眠状态唤醒
|
||||
- 支持强制关闭未响应程序、执行前提醒、撤销系统关机/重启
|
||||
- 使用自定义应用图标,并支持系统托盘运行
|
||||
- 点击窗口关闭按钮会最小化到托盘,只有通过托盘菜单才能真正退出
|
||||
- 支持 OmniNotify 适配模式,可将原本的软件弹窗提示改为发送到 OmniNotify
|
||||
- Debug 构建提供隐藏的界面自截图参数,用于开发者检查 UI
|
||||
- Release 构建不会编译自截图逻辑,保持发布版本轻量
|
||||
|
||||
## 开发
|
||||
|
||||
```powershell
|
||||
dotnet build
|
||||
dotnet run
|
||||
```
|
||||
|
||||
开发 UI 后可以运行隐藏截图命令,自动渲染主界面并保存图片:
|
||||
|
||||
```powershell
|
||||
dotnet run -- --capture-ui=.\Screenshots\ui-review.png
|
||||
dotnet run -- --capture-ui=.\Screenshots\ui-min.png --capture-size=960x720
|
||||
dotnet run -- --capture-ui=.\Screenshots\ui-large.png --capture-size=2048x1152
|
||||
```
|
||||
|
||||
该能力只在 Debug 构建中存在,不会出现在用户界面里。
|
||||
|
||||
## OmniNotify 适配
|
||||
|
||||
在主界面点击“通知设置”,或在托盘菜单中打开“OmniNotify 适配设置”。
|
||||
|
||||
- 软件本身:保留 AutoShutdown 原有弹窗/托盘提示
|
||||
- OmniNotify:将原提示发送到 OmniNotify 的本地接口
|
||||
- 默认监听地址:`http://127.0.0.1:19845/notify`
|
||||
- 默认频道名:`default`
|
||||
|
||||
OmniNotify 需要先运行,并提前创建对应频道。设置保存到 `%LOCALAPPDATA%\AutoShutdown\notification-settings.json`。
|
||||
|
||||
## 发布
|
||||
|
||||
```powershell
|
||||
dotnet publish -c Release -r win-x64
|
||||
```
|
||||
|
||||
发布目录位于 `bin/Release/net8.0-windows/win-x64/publish`。
|
||||
|
||||
## 说明
|
||||
|
||||
普通 Windows 应用无法让已经完全断电的电脑自动开机。应用中的“唤醒”功能依赖 Windows 任务计划程序和系统电源设置,请在“电源选项”中允许唤醒计时器。
|
||||
92
Services/NotificationService.cs
Normal file
92
Services/NotificationService.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using AutoShutdown.Models;
|
||||
|
||||
namespace AutoShutdown.Services;
|
||||
|
||||
internal sealed class NotificationService
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
|
||||
private NotificationSettings _settings;
|
||||
|
||||
public NotificationService(NotificationSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public void UpdateSettings(NotificationSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public async Task NotifyAsync(Window owner, string title, string body, MessageBoxImage image = MessageBoxImage.Information)
|
||||
{
|
||||
if (_settings.Mode == NotificationMode.OmniNotify)
|
||||
{
|
||||
await TrySendToOmniNotifyAsync(title, body);
|
||||
return;
|
||||
}
|
||||
|
||||
System.Windows.MessageBox.Show(owner, body, title, MessageBoxButton.OK, image);
|
||||
}
|
||||
|
||||
public Task NotifyAsync(string title, string body)
|
||||
{
|
||||
return _settings.Mode == NotificationMode.OmniNotify
|
||||
? TrySendToOmniNotifyAsync(title, body)
|
||||
: Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool> TestOmniNotifyAsync(NotificationSettings settings)
|
||||
{
|
||||
var oldSettings = _settings;
|
||||
try
|
||||
{
|
||||
_settings = settings;
|
||||
await SendToOmniNotifyAsync("AutoShutdown 测试消息", "OmniNotify 适配已连接。");
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_settings = oldSettings;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendToOmniNotifyAsync(string title, string body)
|
||||
{
|
||||
if (!Uri.TryCreate(_settings.OmniNotifyEndpoint, UriKind.Absolute, out var endpoint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
channel = string.IsNullOrWhiteSpace(_settings.Channel) ? "default" : _settings.Channel.Trim(),
|
||||
title,
|
||||
body
|
||||
});
|
||||
|
||||
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
|
||||
using var response = await HttpClient.PostAsync(endpoint, content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private async Task TrySendToOmniNotifyAsync(string title, string body)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendToOmniNotifyAsync(title, body);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// OmniNotify 适配模式下不再弹窗打扰用户,发送失败只静默跳过。
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Services/NotificationSettingsStore.cs
Normal file
38
Services/NotificationSettingsStore.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using AutoShutdown.Models;
|
||||
|
||||
namespace AutoShutdown.Services;
|
||||
|
||||
internal sealed class NotificationSettingsStore
|
||||
{
|
||||
private readonly string _path = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"AutoShutdown",
|
||||
"notification-settings.json");
|
||||
|
||||
public NotificationSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_path))
|
||||
{
|
||||
return NotificationSettings.Default();
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(_path);
|
||||
return JsonSerializer.Deserialize<NotificationSettings>(json) ?? NotificationSettings.Default();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return NotificationSettings.Default();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(NotificationSettings settings)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
|
||||
var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(_path, json);
|
||||
}
|
||||
}
|
||||
95
Services/PowerService.cs
Normal file
95
Services/PowerService.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AutoShutdown.Models;
|
||||
|
||||
namespace AutoShutdown.Services;
|
||||
|
||||
internal sealed class PowerService
|
||||
{
|
||||
[DllImport("PowrProf.dll", SetLastError = true)]
|
||||
private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool LockWorkStation();
|
||||
|
||||
public void Execute(PowerAction action, int delaySeconds, bool forceApps)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case PowerAction.Shutdown:
|
||||
RunShutdownCommand($"/s /t {Math.Max(0, delaySeconds)}{ForceFlag(forceApps)}");
|
||||
break;
|
||||
case PowerAction.Restart:
|
||||
RunShutdownCommand($"/r /t {Math.Max(0, delaySeconds)}{ForceFlag(forceApps)}");
|
||||
break;
|
||||
case PowerAction.LogOff:
|
||||
if (delaySeconds > 0)
|
||||
{
|
||||
using var timer = new System.Threading.Timer(_ => RunShutdownCommand($"/l{ForceFlag(forceApps)}"), null, TimeSpan.FromSeconds(delaySeconds), Timeout.InfiniteTimeSpan);
|
||||
Thread.Sleep(TimeSpan.FromSeconds(delaySeconds + 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
RunShutdownCommand($"/l{ForceFlag(forceApps)}");
|
||||
}
|
||||
break;
|
||||
case PowerAction.Lock:
|
||||
DelayThen(delaySeconds, () => LockWorkStation());
|
||||
break;
|
||||
case PowerAction.Sleep:
|
||||
DelayThen(delaySeconds, () => SetSuspendState(false, forceApps, false));
|
||||
break;
|
||||
case PowerAction.Hibernate:
|
||||
DelayThen(delaySeconds, () => SetSuspendState(true, forceApps, false));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CancelShutdown()
|
||||
{
|
||||
var result = RunProcess("shutdown.exe", "/a", showWindow: false);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
private static string ForceFlag(bool forceApps) => forceApps ? " /f" : string.Empty;
|
||||
|
||||
private static void DelayThen(int delaySeconds, Action action)
|
||||
{
|
||||
if (delaySeconds > 0)
|
||||
{
|
||||
Task.Delay(TimeSpan.FromSeconds(delaySeconds)).ContinueWith(_ => action(), TaskScheduler.Default);
|
||||
return;
|
||||
}
|
||||
|
||||
action();
|
||||
}
|
||||
|
||||
private static void RunShutdownCommand(string arguments)
|
||||
{
|
||||
var code = RunProcess("shutdown.exe", arguments, showWindow: false);
|
||||
if (code != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"shutdown.exe 执行失败,退出码 {code}。");
|
||||
}
|
||||
}
|
||||
|
||||
private static int RunProcess(string fileName, string arguments, bool showWindow)
|
||||
{
|
||||
using var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = !showWindow,
|
||||
WindowStyle = showWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden
|
||||
});
|
||||
|
||||
if (process is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
process.WaitForExit();
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
33
Services/ScreenshotService.cs
Normal file
33
Services/ScreenshotService.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
#if DEBUG
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace AutoShutdown.Services;
|
||||
|
||||
internal static class ScreenshotService
|
||||
{
|
||||
public static string Capture(Window window, string? requestedPath = null)
|
||||
{
|
||||
var path = string.IsNullOrWhiteSpace(requestedPath)
|
||||
? Path.Combine(AppContext.BaseDirectory, "Screenshots", $"ui-{DateTime.Now:yyyyMMdd-HHmmss}.png")
|
||||
: Path.GetFullPath(requestedPath);
|
||||
|
||||
var directory = Path.GetDirectoryName(path) ?? AppContext.BaseDirectory;
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var visual = (FrameworkElement)window.Content;
|
||||
var width = Math.Max(1, (int)Math.Ceiling(visual.ActualWidth));
|
||||
var height = Math.Max(1, (int)Math.Ceiling(visual.ActualHeight));
|
||||
var target = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
|
||||
target.Render(visual);
|
||||
|
||||
var encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(target));
|
||||
using var stream = File.Create(path);
|
||||
encoder.Save(stream);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
119
Services/WakeService.cs
Normal file
119
Services/WakeService.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Principal;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace AutoShutdown.Services;
|
||||
|
||||
internal sealed class WakeService
|
||||
{
|
||||
private const string TaskName = "AutoShutdown Wake";
|
||||
|
||||
public void ScheduleWake(DateTime wakeAt)
|
||||
{
|
||||
var taskXmlPath = Path.Combine(Path.GetTempPath(), $"AutoShutdown-Wake-{Guid.NewGuid():N}.xml");
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(taskXmlPath, CreateWakeTaskXml(wakeAt));
|
||||
RunProcess("schtasks.exe", $"/Create /TN \"{TaskName}\" /XML \"{taskXmlPath}\" /F");
|
||||
}
|
||||
finally
|
||||
{
|
||||
TryDelete(taskXmlPath);
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelWake()
|
||||
{
|
||||
RunProcess("schtasks.exe", $"/Delete /TN \"{TaskName}\" /F", ignoreErrors: true);
|
||||
}
|
||||
|
||||
private static string CreateWakeTaskXml(DateTime wakeAt)
|
||||
{
|
||||
XNamespace ns = "http://schemas.microsoft.com/windows/2004/02/mit/task";
|
||||
var startBoundary = wakeAt.ToString("yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture);
|
||||
var userId = WindowsIdentity.GetCurrent().User?.Value ?? Environment.UserName;
|
||||
|
||||
var task = new XDocument(
|
||||
new XElement(ns + "Task",
|
||||
new XAttribute("version", "1.4"),
|
||||
new XElement(ns + "RegistrationInfo",
|
||||
new XElement(ns + "Description", "AutoShutdown wake timer")),
|
||||
new XElement(ns + "Triggers",
|
||||
new XElement(ns + "TimeTrigger",
|
||||
new XElement(ns + "StartBoundary", startBoundary),
|
||||
new XElement(ns + "Enabled", "true"))),
|
||||
new XElement(ns + "Principals",
|
||||
new XElement(ns + "Principal",
|
||||
new XAttribute("id", "Author"),
|
||||
new XElement(ns + "UserId", userId),
|
||||
new XElement(ns + "LogonType", "InteractiveToken"),
|
||||
new XElement(ns + "RunLevel", "LeastPrivilege"))),
|
||||
new XElement(ns + "Settings",
|
||||
new XElement(ns + "MultipleInstancesPolicy", "IgnoreNew"),
|
||||
new XElement(ns + "DisallowStartIfOnBatteries", "false"),
|
||||
new XElement(ns + "StopIfGoingOnBatteries", "false"),
|
||||
new XElement(ns + "AllowHardTerminate", "true"),
|
||||
new XElement(ns + "StartWhenAvailable", "true"),
|
||||
new XElement(ns + "RunOnlyIfNetworkAvailable", "false"),
|
||||
new XElement(ns + "IdleSettings",
|
||||
new XElement(ns + "StopOnIdleEnd", "false"),
|
||||
new XElement(ns + "RestartOnIdle", "false")),
|
||||
new XElement(ns + "AllowStartOnDemand", "true"),
|
||||
new XElement(ns + "Enabled", "true"),
|
||||
new XElement(ns + "Hidden", "true"),
|
||||
new XElement(ns + "RunOnlyIfIdle", "false"),
|
||||
new XElement(ns + "WakeToRun", "true"),
|
||||
new XElement(ns + "ExecutionTimeLimit", "PT1M"),
|
||||
new XElement(ns + "Priority", "7")),
|
||||
new XElement(ns + "Actions",
|
||||
new XAttribute("Context", "Author"),
|
||||
new XElement(ns + "Exec",
|
||||
new XElement(ns + "Command", "cmd.exe"),
|
||||
new XElement(ns + "Arguments", "/c exit")))));
|
||||
|
||||
return task.ToString(SaveOptions.DisableFormatting);
|
||||
}
|
||||
|
||||
private static void RunProcess(string fileName, string arguments, bool ignoreErrors = false)
|
||||
{
|
||||
using var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
});
|
||||
|
||||
if (process is null)
|
||||
{
|
||||
throw new InvalidOperationException("无法启动 Windows 任务计划程序命令。");
|
||||
}
|
||||
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
if (!ignoreErrors && process.ExitCode != 0)
|
||||
{
|
||||
var message = string.Join(Environment.NewLine, new[] { error, output }.Where(item => !string.IsNullOrWhiteSpace(item)));
|
||||
throw new InvalidOperationException(string.IsNullOrWhiteSpace(message) ? "创建唤醒计划失败。" : message.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 临时任务 XML 删除失败不影响计划本身。
|
||||
}
|
||||
}
|
||||
}
|
||||
11
app.manifest
Normal file
11
app.manifest
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="AutoShutdown.app"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user