diff --git a/ClassScheduler.Runner/App.xaml.cs b/ClassScheduler.Runner/App.xaml.cs index f7fbc3a..d5d38f2 100644 --- a/ClassScheduler.Runner/App.xaml.cs +++ b/ClassScheduler.Runner/App.xaml.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.Composition.Hosting; using System.IO; +using System.Reflection; using System.Threading.Tasks; using System.Windows; @@ -30,6 +31,8 @@ private void Application_Startup(object sender, StartupEventArgs e) try { + AddDllResolveHandler(Path.GetFullPath("./")); + LoadPlugin("./ClassScheduler.WPF.dll"); } catch (Exception o) @@ -47,6 +50,23 @@ private void Application_Startup(object sender, StartupEventArgs e) } } + private static void AddDllResolveHandler(string appendPath) + { + var domain = AppDomain.CurrentDomain; + + domain.AssemblyResolve += (sender, args) => + { + var assemblyFolder = Path.GetFullPath(appendPath); + var assemblyPath = Path.Combine(assemblyFolder, new AssemblyName(args.Name).Name + ".dll"); + + if (!File.Exists(assemblyPath)) return null; + + var assembly = Assembly.LoadFrom(assemblyPath); + + return assembly; + }; + } + private static void LoadPlugin(string path) { if (File.Exists(Path.GetFullPath(path))) diff --git a/ClassScheduler.Runner/ClassScheduler.Runner.csproj b/ClassScheduler.Runner/ClassScheduler.Runner.csproj index bb708fd..2ac7942 100644 --- a/ClassScheduler.Runner/ClassScheduler.Runner.csproj +++ b/ClassScheduler.Runner/ClassScheduler.Runner.csproj @@ -6,8 +6,13 @@ enable true AnyCPU;x64 + icon.ico + + + + diff --git a/ClassScheduler.Runner/icon.ico b/ClassScheduler.Runner/icon.ico new file mode 100644 index 0000000..3549db7 Binary files /dev/null and b/ClassScheduler.Runner/icon.ico differ diff --git a/ClassScheduler.WPF/Assets/ChartData.txt b/ClassScheduler.WPF/Assets/ChartData.txt deleted file mode 100644 index a0e9aa3..0000000 --- a/ClassScheduler.WPF/Assets/ChartData.txt +++ /dev/null @@ -1,9 +0,0 @@ -课表-从上至下:周日至周六-从左至右:第1~9节-直接写,无需空格,但只能用一个字表示课程-第9行(最后一行)为高考年份 -7周日课表缺失 -1生班物化-数语英-英英 -2物音生化-英数语-数数 -3数数语语-化生物-语语 -4生语英体-英物化-轮换 -5英英数数-物生化-轮换 -6数语英-化生物 -2023 \ No newline at end of file diff --git a/ClassScheduler.WPF/Assets/icon.ico b/ClassScheduler.WPF/Assets/icon.ico index 02f5e37..3549db7 100644 Binary files a/ClassScheduler.WPF/Assets/icon.ico and b/ClassScheduler.WPF/Assets/icon.ico differ diff --git a/ClassScheduler.WPF/ClassScheduler.WPF.csproj b/ClassScheduler.WPF/ClassScheduler.WPF.csproj index 19becf2..469efe7 100644 --- a/ClassScheduler.WPF/ClassScheduler.WPF.csproj +++ b/ClassScheduler.WPF/ClassScheduler.WPF.csproj @@ -16,6 +16,8 @@ + + @@ -26,9 +28,6 @@ - - Always - Always diff --git a/ClassScheduler.WPF/Converters/HorizontalAlignmentReverseConverter.cs b/ClassScheduler.WPF/Converters/HorizontalAlignmentReverseConverter.cs new file mode 100644 index 0000000..fe25ffc --- /dev/null +++ b/ClassScheduler.WPF/Converters/HorizontalAlignmentReverseConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace ClassScheduler.WPF.Converters; + +public class HorizontalAlignmentReverseConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var align = (HorizontalAlignment)value; + switch (align) + { + case HorizontalAlignment.Left: + return HorizontalAlignment.Right; + case HorizontalAlignment.Center: + return HorizontalAlignment.Stretch; + case HorizontalAlignment.Right: + return HorizontalAlignment.Left; + case HorizontalAlignment.Stretch: + return HorizontalAlignment.Center; + } + + return align; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/ClassScheduler.WPF/Converters/RectConverter.cs b/ClassScheduler.WPF/Converters/RectConverter.cs new file mode 100644 index 0000000..ab3c387 --- /dev/null +++ b/ClassScheduler.WPF/Converters/RectConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace ClassScheduler.WPF.Converters; + +public class RectConverter : IMultiValueConverter +{ + public object Convert( + object[] values, + Type targetType, + object parameter, + CultureInfo culture) + { + return new Rect(0, 0, (double)values[0], (double)values[1]); + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/ClassScheduler.WPF/Data/AppConfig.cs b/ClassScheduler.WPF/Data/AppConfig.cs new file mode 100644 index 0000000..8655ee7 --- /dev/null +++ b/ClassScheduler.WPF/Data/AppConfig.cs @@ -0,0 +1,63 @@ +using ClassScheduler.WPF.Utils; +using System; +using System.IO; +using System.Text.Json; + +namespace ClassScheduler.WPF.Data; + +public class AppConfig +{ + public TopmostEffectsSettings TopmostEffectsSettings { get; set; } = new(); + + public WallPaperSettings WallPaperSettings { get; set; } = new(); +} + +public class TopmostEffectsSettings +{ + public bool? IsDateTimeVisible { get; set; } = true; +} + +public class WallPaperSettings +{ + public string? WallPapersPath { get; set; } + + public int CurrentWallPaperIndex { get; set; } = 0; + + public bool? WallPapersEnabled { get; set; } = false; + + public WallPaperStyle? WallPaperStyle { get; set; } = Utils.WallPaperStyle.Stretched; +} + +public static class AppConfigExtensions +{ + public static void Save( + this AppConfig config, + string path = "./Data/AppConfig.json", + Action? optionsProcessor = null) + { + var options = new JsonSerializerOptions() + { + IncludeFields = true, + WriteIndented = true, + }; + optionsProcessor?.Invoke(options); + + var json = JsonSerializer.Serialize(config, options); + File.WriteAllText(path, json); + } + + public static AppConfig? LoadAsAppConfig( + this string path, + Action? optionsProcessor = null) + { + if (!Path.Exists(path)) return null; + + var options = new JsonSerializerOptions(); + optionsProcessor?.Invoke(options); + + var json = File.ReadAllText(path); + var appConfig = JsonSerializer.Deserialize(json, options); + + return appConfig; + } +} diff --git a/ClassScheduler.WPF/Data/Classes.cs b/ClassScheduler.WPF/Data/Classes.cs index fc54c96..d07375e 100644 --- a/ClassScheduler.WPF/Data/Classes.cs +++ b/ClassScheduler.WPF/Data/Classes.cs @@ -10,25 +10,35 @@ public class Classes { public string? Name { get; set; } - public List ClassesList { get; set; } = new(); + public List ClassesList { get; set; } = new() + { + new() + { + Name = "name", + BeginTime = DateTime.Now, + EndTime = DateTime.Now + new TimeSpan(1,0,0), + WeekDay = 1, + } + }; public Classes Sort() { ClassesList.Sort( - new Comparison( - (x, y) => + (x, y) => + { + if (x.WeekDay == y.WeekDay) { - if (x.WeekDay == y.WeekDay) - { - if (x.BeginTime == y.BeginTime) - return 0; - else - return x.BeginTime > y.BeginTime ? 1 : -1; - } - else - return x.WeekDay > y.WeekDay ? 1 : -1; + var x_begin = DateTime.Parse(x.BeginTime?.ToString("HH:mm")!); + var y_begin = DateTime.Parse(y.BeginTime?.ToString("HH:mm")!); + var x_end = DateTime.Parse(x.EndTime?.ToString("HH:mm")!); + var y_end = DateTime.Parse(y.EndTime?.ToString("HH:mm")!); + + if (x_begin == y_begin && x_end == y_end) + return x.Name!.CompareTo(y.Name); + else return x_begin.CompareTo(y_end); } - ) + else return x.WeekDay > y.WeekDay ? 1 : -1; + } ); return this; @@ -39,7 +49,7 @@ public static class ClassesExtensions { public static void Save( this Classes classes, - string path, + string path = "./Data/Classes.json", Action? optionsProcessor = null) { var options = new JsonSerializerOptions() @@ -49,11 +59,11 @@ public static void Save( }; optionsProcessor?.Invoke(options); - var json = JsonSerializer.Serialize(classes); + var json = JsonSerializer.Serialize(classes, options); File.WriteAllText(path, json); } - public static Classes? Load( + public static Classes? LoadAsClasses( this string path, Action? optionsProcessor = null) { @@ -63,7 +73,7 @@ public static void Save( optionsProcessor?.Invoke(options); var json = File.ReadAllText(path); - var classes = JsonSerializer.Deserialize(json); + var classes = JsonSerializer.Deserialize(json, options); return classes; } diff --git a/ClassScheduler.WPF/Identity.cs b/ClassScheduler.WPF/Identity.cs index d2dba39..e52d410 100644 --- a/ClassScheduler.WPF/Identity.cs +++ b/ClassScheduler.WPF/Identity.cs @@ -1,6 +1,6 @@ using KitX.Contract.CSharp; -using System.Collections.Generic; using System; +using System.Collections.Generic; namespace ClassScheduler.WPF; diff --git a/ClassScheduler.WPF/Instances.cs b/ClassScheduler.WPF/Instances.cs index 411f97b..1387ad1 100644 --- a/ClassScheduler.WPF/Instances.cs +++ b/ClassScheduler.WPF/Instances.cs @@ -12,14 +12,22 @@ internal static void Init() if (!Directory.Exists(Path.GetFullPath("./Data/"))) Directory.CreateDirectory(Path.GetFullPath("./Data/")); - Classes = "./Data/Classes.json".Load(); + AppConfig = "./Data/AppConfig.json".LoadAsAppConfig(); + AppConfig ??= new(); + AppConfig.Save(); + + Classes = "./Data/Classes.json".LoadAsClasses(); Classes ??= new(); - Classes.Save("./Data/Classes.json"); + Classes.Sort(); + Classes.Save(); MainWindow = new(); ScheduleWindow = new(); + TopmostEffectsWindow = new(); + TopmostEffectsWindow.Show(); + Controller = new(); } @@ -27,8 +35,12 @@ internal static void Init() internal static ScheduleWindow? ScheduleWindow { get; set; } + internal static TopmostEffectsWindow? TopmostEffectsWindow { get; set; } + internal static Controller? Controller { get; set; } + internal static AppConfig? AppConfig { get; set; } + internal static Classes? Classes { get; set; } internal static NotifyIcon? NotifyIcon { get; set; } diff --git a/ClassScheduler.WPF/Models/ClassModel.cs b/ClassScheduler.WPF/Models/ClassModel.cs index 6c703a9..f6bdc82 100644 --- a/ClassScheduler.WPF/Models/ClassModel.cs +++ b/ClassScheduler.WPF/Models/ClassModel.cs @@ -1,6 +1,5 @@ -using System; -using System.IO; -using ClassScheduler.WPF.Models.Types; +using ClassScheduler.WPF.Models.Types; +using System; namespace ClassScheduler.WPF.Models; @@ -33,7 +32,7 @@ public int? DayOfWeek { var weekDay = (int)WeekDay!; var dividedCount = 0; - while(weekDay != 1) + while (weekDay != 1) { weekDay /= 2; ++dividedCount; diff --git a/ClassScheduler.WPF/Utils/Converter/DayOfWeekToInt.cs b/ClassScheduler.WPF/Utils/Converter/DayOfWeekToInt.cs new file mode 100644 index 0000000..ece3c75 --- /dev/null +++ b/ClassScheduler.WPF/Utils/Converter/DayOfWeekToInt.cs @@ -0,0 +1,29 @@ +using System; + +namespace ClassScheduler.WPF.Utils.Converter; + +public static class DayOfWeekToInt +{ + public static int ToInt(this DayOfWeek day) + { + switch (day) + { + case DayOfWeek.Sunday: + return 7; + case DayOfWeek.Monday: + return 1; + case DayOfWeek.Tuesday: + return 2; + case DayOfWeek.Wednesday: + return 3; + case DayOfWeek.Thursday: + return 4; + case DayOfWeek.Friday: + return 5; + case DayOfWeek.Saturday: + return 6; + } + + return -1; + } +} diff --git a/ClassScheduler.WPF/Utils/FixedSizedQueue.cs b/ClassScheduler.WPF/Utils/FixedSizedQueue.cs new file mode 100644 index 0000000..b834ac5 --- /dev/null +++ b/ClassScheduler.WPF/Utils/FixedSizedQueue.cs @@ -0,0 +1,28 @@ +using System.Collections.Concurrent; + +namespace ClassScheduler.WPF.Utils; + +public class FixedSizedQueue : ConcurrentQueue +{ + private readonly object syncObject = new(); + + public int Size { get; private set; } + + public FixedSizedQueue(int size) + { + Size = size; + } + + public new void Enqueue(T obj) + { + base.Enqueue(obj); + + lock (syncObject) + { + while (Count > Size) + { + TryDequeue(out T outObj); + } + } + } +} diff --git a/ClassScheduler.WPF/Utils/NotifyIconManager.cs b/ClassScheduler.WPF/Utils/NotifyIconManager.cs new file mode 100644 index 0000000..f9da1f9 --- /dev/null +++ b/ClassScheduler.WPF/Utils/NotifyIconManager.cs @@ -0,0 +1,143 @@ +using ClassScheduler.WPF.Data; +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace ClassScheduler.WPF.Utils; + +internal static class NotifyIconManager +{ + private static ToolStripMenuItem BuildItem( + string text = "", + Action? onClick = null, + Action? onCheckChanged = null, + bool? checkOnClick = null, + CheckState? checkState = null, + List? subItems = null) + { + var toolStripMenuItem = new ToolStripMenuItem() + { + Text = text, + CheckOnClick = checkOnClick ?? false, + CheckState = checkState ?? CheckState.Unchecked, + }; + + toolStripMenuItem.Click += (x, y) => onClick?.Invoke(x, y); + toolStripMenuItem.CheckedChanged += (x, y) => onCheckChanged?.Invoke(x, y); + + if (subItems is not null) + foreach (var item in subItems) + toolStripMenuItem.DropDownItems.Add(item); + + return toolStripMenuItem; + } + + private static ContextMenuStrip BuildContextMenu() + { + var contextMenuStrip = new ContextMenuStrip(); + contextMenuStrip.Items.Add(BuildItem( + "调试", + subItems: new() + { + BuildItem( + "播放预备铃动画", + onClick: (_, _) => Instances.TopmostEffectsWindow!.PlayPrepareClassAlert() + ), + BuildItem( + "播放上课动画", + onClick: (_, _) => Instances.ScheduleWindow!.PlayClassBeginAnimation() + ), + BuildItem( + "播放下课动画", + onClick: (_, _) => Instances.ScheduleWindow!.PlayClassOverAnimation() + ), + BuildItem( + "立即刷新天气数据", + onClick: (_, _) => Instances.ScheduleWindow!.RefreshWeather() + ), + BuildItem( + "立即刷新一言", + onClick: (_, _) => Instances.ScheduleWindow!.RefreshSentence() + ), + } + )); + contextMenuStrip.Items.Add(new ToolStripSeparator()); + contextMenuStrip.Items.Add(BuildItem( + "置顶窗口", + subItems: new() + { + BuildItem( + "显示时钟", + checkOnClick: true, + checkState: (Instances.AppConfig!.TopmostEffectsSettings.IsDateTimeVisible ?? true) + ? CheckState.Checked : CheckState.Unchecked, + onCheckChanged: (sender, _) => + Instances.TopmostEffectsWindow!.SetDateTimeVisibility( + (sender as ToolStripMenuItem)!.Checked + ) + ), + } + )); + contextMenuStrip.Items.Add(BuildItem( + "浏览器", + subItems: new() + { + BuildItem( + "显示背景浏览器", + checkOnClick: true, + onCheckChanged: (sender, _) => + Instances.ScheduleWindow!.SetWebViewVisibility( + (sender as ToolStripMenuItem)!.Checked + ) + ), + BuildItem( + "刷新网页", + onClick: (_, _) => Instances.ScheduleWindow!.GetWebView().Reload() + ) + } + )); + contextMenuStrip.Items.Add(BuildItem( + "壁纸", + subItems: new() + { + BuildItem( + "下一张", + onClick: (_, _) => Instances.MainWindow!.NextWallPaper() + ) + } + )); + contextMenuStrip.Items.Add(BuildItem( + "退出", + onClick: (_, _) => + { + Instances.NotifyIcon!.Dispose(); + Instances.MainWindow!.Exit(); + } + )); + return contextMenuStrip; + } + + internal static void BuildNotifyIcon() + { + Instances.NotifyIcon = new NotifyIcon() + { + Icon = new System.Drawing.Icon($"{GlobalData.RootPath}/Assets/icon.ico"), + Visible = true, + Text = "ClassScheduler", + ContextMenuStrip = BuildContextMenu(), + }; + Instances.NotifyIcon.MouseClick += (_, e) => + { + if (e.Button != MouseButtons.Left) + return; + + Instances.MainWindow!.ComplexShow(); + }; + } + + internal static void Rebuild() + { + Instances.NotifyIcon!.Dispose(); + BuildNotifyIcon(); + } +} diff --git a/ClassScheduler.WPF/Utils/User32.cs b/ClassScheduler.WPF/Utils/User32.cs index bff7b50..75620dd 100644 --- a/ClassScheduler.WPF/Utils/User32.cs +++ b/ClassScheduler.WPF/Utils/User32.cs @@ -1,4 +1,6 @@ -using System; +using Microsoft.Win32; +using System; +using System.IO; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; @@ -6,7 +8,7 @@ namespace ClassScheduler.WPF.Utils; [Flags] -public enum SendMessageTimeoutFlags : uint +public enum SendMessageTimeoutFlags : uint { SMTO_NORMAL = 0x0, SMTO_BLOCK = 0x1, @@ -15,6 +17,13 @@ public enum SendMessageTimeoutFlags : uint SMTO_ERRORONEXIT = 0x20 } +public enum WallPaperStyle : int +{ + Tiled, + Centered, + Stretched +} + public partial class User32 { private const string user32dllPath = "user32.dll"; @@ -68,6 +77,13 @@ public partial class User32 internal const int GWL_HWNDPARENT = -8; internal static readonly IntPtr HWND_BOTTOM = new(1); + + internal const int SPI_SETDESKWALLPAPER = 20; + internal const int SPIF_UPDATEINIFILE = 0x01; + internal const int SPIF_SENDWININICHANGE = 0x02; + + [DllImport(user32dllPath, CharSet = CharSet.Auto)] + internal static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); } public static class User32Extensions @@ -152,4 +168,34 @@ public static void BottomMost(this Window window) ); _ = User32.SetWindowLong(handle, User32.GWL_HWNDPARENT, hprog); } + + public static void SetWallPaper(this string path, WallPaperStyle style) + { + var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Desktop", true); + + if (style == WallPaperStyle.Stretched) + { + key?.SetValue(@"WallpaperStyle", 2.ToString()); + key?.SetValue(@"TileWallpaper", 0.ToString()); + } + + if (style == WallPaperStyle.Centered) + { + key?.SetValue(@"WallpaperStyle", 1.ToString()); + key?.SetValue(@"TileWallpaper", 0.ToString()); + } + + if (style == WallPaperStyle.Tiled) + { + key?.SetValue(@"WallpaperStyle", 1.ToString()); + key?.SetValue(@"TileWallpaper", 1.ToString()); + } + + User32.SystemParametersInfo( + User32.SPI_SETDESKWALLPAPER, + 0, + Path.GetFullPath(path), + User32.SPIF_UPDATEINIFILE | User32.SPIF_SENDWININICHANGE + ); + } } diff --git a/ClassScheduler.WPF/Views/MainWindow.xaml b/ClassScheduler.WPF/Views/MainWindow.xaml index 272cb15..d0ca460 100644 --- a/ClassScheduler.WPF/Views/MainWindow.xaml +++ b/ClassScheduler.WPF/Views/MainWindow.xaml @@ -5,51 +5,80 @@ xmlns:local="clr-namespace:ClassScheduler.WPF.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="ClassScheduler.WPF" - Width="460" + Width="440" Height="600" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> - - - - - - - - - - - -