diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0516f0..a90a89f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Check out source - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Build test executable shell: powershell @@ -44,7 +44,7 @@ jobs: Compress-Archive -Path release\* -DestinationPath ConferenceDDL-Windows.zip - name: Upload Windows artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ConferenceDDL-Windows path: | diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs index e88619b..c3aaff3 100644 --- a/AssemblyInfo.cs +++ b/AssemblyInfo.cs @@ -6,6 +6,6 @@ [assembly: AssemblyCompany("wenan4")] [assembly: AssemblyProduct("Conference DDL")] [assembly: AssemblyCopyright("© 2026 wenan4. CCFDDL data © CCFDDL contributors")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.1.0.0")] +[assembly: AssemblyFileVersion("1.1.0.0")] [assembly: ComVisible(false)] diff --git a/CHANGELOG.md b/CHANGELOG.md index 748a603..c4798e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.1.0 — 2026-08-01 + +- Discover every CCF-A conference currently represented in the upstream CCFDDL data. +- Keep the original nine conferences as the default selection, including ECCV and SIGGRAPH Asia. +- Add a searchable, category-grouped conference picker with default, all-CCF-A, and clear actions. +- Add an upstream-driven robotics collection for ICRA, IROS, RSS, and CoRL. +- Persist the selected conference list in the local Windows profile. + ## 1.0.0 — 2026-08-01 - Initial public release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 638a58b..077979e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,9 @@ Thanks for helping improve Conference DDL. ## Before opening a change - Use an issue for incorrect or missing deadlines and include the official conference page. -- Keep the tracked conference list focused on the nine series documented in the README unless a change is discussed first. +- Keep the original nine default selections stable unless a change is discussed first. +- Additional conference availability is derived from upstream `rank.ccf: A`; avoid maintaining a second hard-coded CCF-A list. +- Robotics exceptions maintain names only (`ICRA`, `IROS`, `RSS`, `CoRL`); deadlines and years must continue to come from CCFDDL. - Treat `deadline` and `abstract_deadline` as different fields; the widget intentionally displays only the full-paper deadline. - Keep the interface quiet, light, keyboard/mouse friendly, and day-level rather than second-level. diff --git a/ConferenceDataService.cs b/ConferenceDataService.cs index 7effe28..f743691 100644 --- a/ConferenceDataService.cs +++ b/ConferenceDataService.cs @@ -35,35 +35,47 @@ internal async Task LoadAsync(DateTimeOffset now) if (!String.IsNullOrWhiteSpace(remoteData)) { List editions = ParseYaml(remoteData); - List items = SelectItems(editions, now); - if (items.Count == ConferenceCatalog.OrderedNames.Length) + DataLoadResult onlineResult = TryCreateResult(editions, now, "CCFDDL 在线数据", false); + if (onlineResult != null) { SaveCache(remoteData); - return CreateResult(items, "CCFDDL 在线数据", false); + return onlineResult; } } string cachedData = ReadCache(); if (!String.IsNullOrWhiteSpace(cachedData)) { - List cachedItems = SelectItems(ParseYaml(cachedData), now); - if (cachedItems.Count == ConferenceCatalog.OrderedNames.Length) + DataLoadResult cachedResult = TryCreateResult( + ParseYaml(cachedData), now, "本地缓存", true); + if (cachedResult != null) { - return CreateResult(cachedItems, "本地缓存", true); + return cachedResult; } } - List fallbackItems = SelectItems(ParseYaml(FallbackYaml), now); - return CreateResult(fallbackItems, "内置备用数据", true); + List fallbackEditions = ParseYaml(FallbackYaml); + return TryCreateResult(fallbackEditions, now, "内置备用数据", true); } - private static DataLoadResult CreateResult( - List items, + private static DataLoadResult TryCreateResult( + List editions, + DateTimeOffset now, string sourceLabel, bool isOffline) { + List conferences = GetSelectableConferences(editions); + if (conferences.Count < ConferenceCatalog.DefaultNames.Length) + { + return null; + } + DataLoadResult result = new DataLoadResult(); - result.Items = items; + result.Conferences = conferences; + result.Items = SelectItems( + editions, + conferences.Select(delegate(ConferenceDefinition item) { return item.Name; }), + now); result.SourceLabel = sourceLabel; result.LoadedAt = DateTime.Now; result.IsOffline = isOffline; @@ -78,7 +90,7 @@ private static async Task TryDownloadAsync() using (HttpClient client = new HttpClient(handler)) { client.Timeout = TimeSpan.FromSeconds(12); - client.DefaultRequestHeaders.UserAgent.ParseAdd("ConferenceDeadlineWidget/1.0"); + client.DefaultRequestHeaders.UserAgent.ParseAdd("ConferenceDeadlineWidget/1.1"); for (int i = 0; i < SourceUrls.Length; i++) { @@ -147,9 +159,11 @@ internal static List ParseYaml(string yaml) } string currentSeries = null; + string currentDescription = null; + string currentCategory = null; + string currentCcfRank = null; ConferenceEdition currentEdition = null; DeadlineValue lastDeadline = null; - bool targetBlock = false; bool inTimeline = false; string normalized = yaml.Replace("\r\n", "\n").Replace('\r', '\n'); @@ -168,25 +182,46 @@ internal static List ParseYaml(string yaml) if (indent == 0 && trimmed.StartsWith("- title:", StringComparison.Ordinal)) { - AddEdition(editions, currentEdition, targetBlock); + AddEdition(editions, currentEdition); currentEdition = null; currentSeries = Unquote(ValueAfterColon(trimmed)); - targetBlock = ConferenceCatalog.IsTarget(currentSeries); + currentDescription = null; + currentCategory = null; + currentCcfRank = null; inTimeline = false; lastDeadline = null; continue; } - if (!targetBlock) + if (currentEdition == null && indent == 2 + && trimmed.StartsWith("description:", StringComparison.Ordinal)) { + currentDescription = Unquote(ValueAfterColon(trimmed)); + continue; + } + + if (currentEdition == null && indent == 2 + && trimmed.StartsWith("sub:", StringComparison.Ordinal)) + { + currentCategory = Unquote(ValueAfterColon(trimmed)); + continue; + } + + if (currentEdition == null && indent == 4 + && trimmed.StartsWith("ccf:", StringComparison.Ordinal)) + { + currentCcfRank = Unquote(ValueAfterColon(trimmed)); continue; } if (indent == 2 && trimmed.StartsWith("- year:", StringComparison.Ordinal)) { - AddEdition(editions, currentEdition, true); + AddEdition(editions, currentEdition); currentEdition = new ConferenceEdition(); currentEdition.Series = currentSeries; + currentEdition.Description = currentDescription; + currentEdition.CategoryCode = currentCategory; + currentEdition.CcfRank = currentCcfRank; Int32.TryParse(Unquote(ValueAfterColon(trimmed)), out currentEdition.Year); inTimeline = false; lastDeadline = null; @@ -268,7 +303,7 @@ internal static List ParseYaml(string yaml) } } - AddEdition(editions, currentEdition, targetBlock); + AddEdition(editions, currentEdition); return editions; } @@ -285,10 +320,9 @@ private static int CountIndent(string line) private static void AddEdition( List editions, - ConferenceEdition edition, - bool targetBlock) + ConferenceEdition edition) { - if (targetBlock && edition != null && edition.Year > 0) + if (edition != null && edition.Year > 0 && !String.IsNullOrWhiteSpace(edition.Series)) { editions.Add(edition); } @@ -326,15 +360,84 @@ private static string Unquote(string value) return result; } + internal static List GetSelectableConferences( + List editions) + { + Dictionary definitions = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < editions.Count; i++) + { + ConferenceEdition edition = editions[i]; + bool selectable = ConferenceCatalog.IsDefault(edition.Series) + || ConferenceCatalog.IsRoboticsFeatured(edition.Series) + || String.Equals(edition.CcfRank, "A", StringComparison.OrdinalIgnoreCase); + if (!selectable || definitions.ContainsKey(edition.Series)) + { + continue; + } + + ConferenceDefinition definition = new ConferenceDefinition(); + definition.Name = edition.Series; + definition.Description = edition.Description; + definition.IsRoboticsFeatured = ConferenceCatalog.IsRoboticsFeatured(edition.Series); + definition.CategoryCode = definition.IsRoboticsFeatured ? "RB" : edition.CategoryCode; + definition.CcfRank = edition.CcfRank; + definition.IsOriginalDefault = ConferenceCatalog.IsDefault(edition.Series); + definitions[definition.Name] = definition; + } + + List defaults = ConferenceCatalog.CreateDefaultDefinitions(); + for (int i = 0; i < defaults.Count; i++) + { + ConferenceDefinition fallback = defaults[i]; + ConferenceDefinition existing; + if (!definitions.TryGetValue(fallback.Name, out existing)) + { + definitions[fallback.Name] = fallback; + continue; + } + + existing.IsOriginalDefault = true; + if (String.IsNullOrWhiteSpace(existing.Description)) + { + existing.Description = fallback.Description; + } + if (String.IsNullOrWhiteSpace(existing.CategoryCode)) + { + existing.CategoryCode = fallback.CategoryCode; + } + if (String.IsNullOrWhiteSpace(existing.CcfRank)) + { + existing.CcfRank = fallback.CcfRank; + } + } + + return definitions.Values + .OrderBy(delegate(ConferenceDefinition item) + { + return ConferenceCatalog.CategoryOrder(item.CategoryCode); + }) + .ThenBy(delegate(ConferenceDefinition item) { return item.Name; }) + .ToList(); + } + internal static List SelectItems( List editions, DateTimeOffset now) + { + return SelectItems(editions, ConferenceCatalog.DefaultNames, now); + } + + internal static List SelectItems( + List editions, + IEnumerable names, + DateTimeOffset now) { List items = new List(); - for (int nameIndex = 0; nameIndex < ConferenceCatalog.OrderedNames.Length; nameIndex++) + foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase)) { - string name = ConferenceCatalog.OrderedNames[nameIndex]; List matching = editions .Where(delegate(ConferenceEdition edition) { @@ -517,7 +620,7 @@ internal static int RunSelfTest() List parsed = ParseYaml(FallbackYaml); List selected = SelectItems(parsed, now); - if (selected.Count != 9) + if (selected.Count != ConferenceCatalog.DefaultNames.Length) { throw new InvalidOperationException("Expected nine conference rows."); } @@ -550,7 +653,58 @@ internal static int RunSelfTest() throw new InvalidOperationException("SIGGRAPH Asia must remain a separate conference."); } - Console.WriteLine("PASS: parsed 9 conferences, ignored abstract deadlines, and converted timezones."); + const string catalogYaml = @" +- title: TEST-A + description: Test CCF-A Conference + sub: DB + rank: + ccf: A + confs: + - year: 2027 + link: https://example.com/a + timeline: + - deadline: '2026-12-01 23:59:59' + timezone: UTC+0 +- title: TEST-B + description: Test CCF-B Conference + sub: DB + rank: + ccf: B + confs: + - year: 2027 + link: https://example.com/b + timeline: + - deadline: '2026-12-02 23:59:59' + timezone: UTC+0 +- title: ICRA + description: IEEE International Conference on Robotics and Automation + sub: AI + rank: + ccf: B + confs: + - year: 2027 + link: https://2027.ieee-icra.org/ + timeline: + - deadline: '2026-09-15 23:59:59' + timezone: UTC-7 +"; + List discovered = GetSelectableConferences(ParseYaml(catalogYaml)); + if (!discovered.Any(delegate(ConferenceDefinition item) { return item.Name == "TEST-A"; }) + || discovered.Any(delegate(ConferenceDefinition item) { return item.Name == "TEST-B"; })) + { + throw new InvalidOperationException("CCF-A catalog filtering is incorrect."); + } + ConferenceDefinition robotics = discovered.FirstOrDefault( + delegate(ConferenceDefinition item) { return item.Name == "ICRA"; }); + if (robotics == null || !robotics.IsRoboticsFeatured || robotics.CategoryCode != "RB") + { + throw new InvalidOperationException("Robotics featured catalog is incorrect."); + } + + ConferencePreferences.ValidateRoundTripForTest(); + + Console.WriteLine( + "PASS: parsed defaults, discovered CCF-A and robotics conferences, persisted selection, ignored abstract deadlines, and converted timezones."); return 0; } catch (Exception exception) @@ -565,12 +719,21 @@ internal static int RunFileTest(string yaml, DateTimeOffset now) try { List parsed = ParseYaml(yaml); - List selected = SelectItems(parsed, now); - if (selected.Count != ConferenceCatalog.OrderedNames.Length) - { - throw new InvalidOperationException("Expected nine conference rows."); + List conferences = GetSelectableConferences(parsed); + List selected = SelectItems( + parsed, + conferences.Select(delegate(ConferenceDefinition item) { return item.Name; }), + now); + if (selected.Count != conferences.Count) + { + throw new InvalidOperationException("Selectable conference count mismatch."); } + + Console.WriteLine( + "Selectable conferences: " + + conferences.Count.ToString(CultureInfo.InvariantCulture)); + for (int i = 0; i < selected.Count; i++) { ConferenceDisplayItem item = selected[i]; diff --git a/ConferencePreferences.cs b/ConferencePreferences.cs new file mode 100644 index 0000000..11a6e1e --- /dev/null +++ b/ConferencePreferences.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace ConferenceDeadlineWidget +{ + internal sealed class ConferencePreferences + { + private readonly string settingsPath; + private readonly bool isTransient; + + private ConferencePreferences(string path, bool transient) + { + settingsPath = path; + isTransient = transient; + SelectedNames = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + internal HashSet SelectedNames { get; private set; } + + internal static ConferencePreferences Load() + { + string directory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "ConferenceDeadlineWidget"); + return LoadFromPath(Path.Combine(directory, "settings.txt"), false); + } + + private static ConferencePreferences LoadFromPath(string path, bool transient) + { + ConferencePreferences preferences = new ConferencePreferences(path, transient); + + if (!File.Exists(preferences.settingsPath)) + { + preferences.SetDefaults(); + return preferences; + } + + try + { + string[] lines = File.ReadAllLines(preferences.settingsPath, Encoding.UTF8); + for (int i = 0; i < lines.Length; i++) + { + const string prefix = "conference="; + string line = lines[i].Trim(); + if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + string name = line.Substring(prefix.Length).Trim(); + if (name.Length > 0) + { + preferences.SelectedNames.Add(name); + } + } + } + return preferences; + } + catch + { + preferences.SetDefaults(); + return preferences; + } + } + + internal static void ValidateRoundTripForTest() + { + string path = Path.Combine( + Path.GetTempPath(), + "ConferenceDDL-preferences-" + Guid.NewGuid().ToString("N") + ".txt"); + try + { + ConferencePreferences saved = new ConferencePreferences(path, false); + saved.ReplaceSelected(new[] { "ACL", "SIGMOD" }); + saved.Save(); + + ConferencePreferences loaded = LoadFromPath(path, false); + if (loaded.SelectedNames.Count != 2 + || !loaded.SelectedNames.Contains("ACL") + || !loaded.SelectedNames.Contains("SIGMOD")) + { + throw new InvalidOperationException("Conference preference persistence is incorrect."); + } + } + finally + { + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + // A locked temporary test file can be left for the OS to clean up. + } + } + } + + internal static ConferencePreferences CreateTransientDefaults() + { + ConferencePreferences preferences = new ConferencePreferences(null, true); + preferences.SetDefaults(); + return preferences; + } + + internal void ReplaceSelected(IEnumerable selectedNames) + { + SelectedNames = new HashSet( + selectedNames ?? Enumerable.Empty(), + StringComparer.OrdinalIgnoreCase); + } + + internal void Save() + { + if (isTransient || String.IsNullOrWhiteSpace(settingsPath)) + { + return; + } + + try + { + string directory = Path.GetDirectoryName(settingsPath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + StringBuilder content = new StringBuilder(); + content.AppendLine("version=1"); + foreach (string name in SelectedNames.OrderBy(delegate(string item) { return item; })) + { + content.Append("conference="); + content.AppendLine(name); + } + File.WriteAllText(settingsPath, content.ToString(), new UTF8Encoding(false)); + } + catch + { + // Preferences are optional; a read-only profile must not stop the widget. + } + } + + private void SetDefaults() + { + SelectedNames.Clear(); + for (int i = 0; i < ConferenceCatalog.DefaultNames.Length; i++) + { + SelectedNames.Add(ConferenceCatalog.DefaultNames[i]); + } + } + } +} diff --git a/ConferenceSelectionWindow.cs b/ConferenceSelectionWindow.cs new file mode 100644 index 0000000..2234dca --- /dev/null +++ b/ConferenceSelectionWindow.cs @@ -0,0 +1,558 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Effects; + +namespace ConferenceDeadlineWidget +{ + internal sealed class ConferenceSelectionWindow : Window + { + private readonly List rows; + private readonly List groups; + private readonly HashSet preservedUnknownSelections; + private TextBox searchBox; + private TextBlock searchPlaceholder; + private readonly TextBlock selectedCountText; + + internal ConferenceSelectionWindow( + List definitions, + IEnumerable selectedNames) + { + rows = new List(); + groups = new List(); + HashSet initialSelection = new HashSet( + selectedNames ?? Enumerable.Empty(), + StringComparer.OrdinalIgnoreCase); + preservedUnknownSelections = new HashSet( + initialSelection, + StringComparer.OrdinalIgnoreCase); + + Title = "选择会议"; + Width = 590; + Height = 680; + WindowStyle = WindowStyle.None; + ResizeMode = ResizeMode.NoResize; + AllowsTransparency = true; + Background = Brushes.Transparent; + ShowInTaskbar = false; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + + Border shell = new Border(); + shell.CornerRadius = new CornerRadius(18); + shell.Background = BrushFrom("#FFF9FBFD"); + shell.BorderBrush = BrushFrom("#FFB9D0E5"); + shell.BorderThickness = new Thickness(1); + shell.Effect = new DropShadowEffect + { + Color = Color.FromRgb(74, 96, 120), + BlurRadius = 26, + ShadowDepth = 7, + Opacity = 0.28 + }; + Content = shell; + + Grid root = new Grid(); + root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(76) }); + root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(54) }); + root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(70) }); + shell.Child = root; + + root.Children.Add(BuildHeader(definitions)); + + Grid toolbar = BuildToolbar(); + Grid.SetRow(toolbar, 1); + root.Children.Add(toolbar); + + ScrollViewer scroller = new ScrollViewer(); + scroller.Margin = new Thickness(18, 0, 18, 0); + scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + StackPanel list = new StackPanel(); + scroller.Content = list; + BuildConferenceRows(list, definitions, initialSelection); + Grid.SetRow(scroller, 2); + root.Children.Add(scroller); + + Grid footer = new Grid(); + footer.Margin = new Thickness(18, 12, 18, 16); + footer.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + footer.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + selectedCountText = MakeText(String.Empty, 11, FontWeights.Normal, "#FF6F849A"); + selectedCountText.VerticalAlignment = VerticalAlignment.Center; + footer.Children.Add(selectedCountText); + + StackPanel footerButtons = new StackPanel(); + footerButtons.Orientation = Orientation.Horizontal; + Button cancelButton = MakeActionButton("取消", false); + cancelButton.Click += delegate { DialogResult = false; }; + footerButtons.Children.Add(cancelButton); + Button saveButton = MakeActionButton("保存选择", true); + saveButton.Margin = new Thickness(8, 0, 0, 0); + saveButton.Click += SaveSelection; + footerButtons.Children.Add(saveButton); + Grid.SetColumn(footerButtons, 1); + footer.Children.Add(footerButtons); + Grid.SetRow(footer, 3); + root.Children.Add(footer); + + UpdateSelectedCount(); + } + + internal HashSet SelectedNames { get; private set; } + + internal bool ValidateBehaviorForTest(out string error) + { + if (rows.Count == 0) + { + error = "Conference selection list is empty."; + return false; + } + + int expectedDefaults = rows.Count(delegate(SelectionRow row) + { + return ConferenceCatalog.IsDefault(row.Definition.Name); + }); + int initiallySelected = rows.Count(delegate(SelectionRow row) + { + return row.CheckBox.IsChecked == true; + }); + if (initiallySelected != expectedDefaults) + { + error = "Default conference selection is incorrect."; + return false; + } + + int expectedCcfA = rows.Count(delegate(SelectionRow row) + { + return String.Equals(row.Definition.CcfRank, "A", StringComparison.OrdinalIgnoreCase); + }); + SelectAllCcfA(); + int selectedCcfA = rows.Count(delegate(SelectionRow row) + { + return row.CheckBox.IsChecked == true; + }); + if (selectedCcfA != expectedCcfA) + { + error = "Select-all CCF-A action is incorrect."; + return false; + } + + int expectedRobotics = rows.Count(delegate(SelectionRow row) + { + return row.Definition.IsRoboticsFeatured; + }); + SelectRoboticsFeatured(); + int selectedRobotics = rows.Count(delegate(SelectionRow row) + { + return row.CheckBox.IsChecked == true; + }); + if (selectedRobotics != expectedRobotics) + { + error = "Robotics featured selection action is incorrect."; + return false; + } + + searchBox.Text = rows[0].Definition.Name; + if (!rows.Any(delegate(SelectionRow row) + { return row.Container.Visibility == Visibility.Visible; })) + { + error = "Conference search hid every matching row."; + return false; + } + searchBox.Text = String.Empty; + + ClearSelection(); + if (rows.Any(delegate(SelectionRow row) { return row.CheckBox.IsChecked == true; })) + { + error = "Clear-selection action is incorrect."; + return false; + } + + SelectDefaults(); + error = null; + return true; + } + + private Grid BuildHeader(List definitions) + { + Grid header = new Grid(); + header.Margin = new Thickness(20, 14, 14, 8); + header.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + int ccfACount = definitions.Count(delegate(ConferenceDefinition item) + { + return String.Equals(item.CcfRank, "A", StringComparison.OrdinalIgnoreCase); + }); + int roboticsCount = definitions.Count(delegate(ConferenceDefinition item) + { + return item.IsRoboticsFeatured; + }); + StackPanel title = new StackPanel(); + TextBlock heading = MakeText("选择要显示的会议", 19, FontWeights.Bold, "#FF2B3E55"); + TextBlock subtitle = MakeText( + ccfACount.ToString() + " 个 CCF-A · " + + roboticsCount.ToString() + " 个机器人精选 · 保留默认 9 个", + 11, + FontWeights.Normal, + "#FF768BA3"); + subtitle.Margin = new Thickness(0, 4, 0, 0); + title.Children.Add(heading); + title.Children.Add(subtitle); + header.Children.Add(title); + + Button close = MakeIconButton("×", "关闭"); + close.Click += delegate { DialogResult = false; }; + Grid.SetColumn(close, 1); + header.Children.Add(close); + return header; + } + + private Grid BuildToolbar() + { + Grid toolbar = new Grid(); + toolbar.Margin = new Thickness(18, 3, 18, 10); + toolbar.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + toolbar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + Border searchShell = new Border(); + searchShell.Background = BrushFrom("#FFF0F5FA"); + searchShell.BorderBrush = BrushFrom("#FFD5E2EE"); + searchShell.BorderThickness = new Thickness(1); + searchShell.CornerRadius = new CornerRadius(10); + searchShell.Padding = new Thickness(10, 2, 8, 2); + searchBox = new TextBox(); + searchBox.BorderThickness = new Thickness(0); + searchBox.Background = Brushes.Transparent; + searchBox.FontFamily = new FontFamily("Microsoft YaHei UI"); + searchBox.FontSize = 12; + searchBox.Foreground = BrushFrom("#FF40566D"); + searchBox.VerticalContentAlignment = VerticalAlignment.Center; + searchBox.ToolTip = "搜索简称、全称或领域"; + searchBox.TextChanged += delegate { ApplySearch(); }; + Grid searchContent = new Grid(); + searchPlaceholder = MakeText( + "搜索会议、简称或领域", + 11, + FontWeights.Normal, + "#FF91A2B4"); + searchPlaceholder.VerticalAlignment = VerticalAlignment.Center; + searchPlaceholder.IsHitTestVisible = false; + searchContent.Children.Add(searchPlaceholder); + searchContent.Children.Add(searchBox); + searchShell.Child = searchContent; + toolbar.Children.Add(searchShell); + + StackPanel actions = new StackPanel(); + actions.Orientation = Orientation.Horizontal; + actions.Margin = new Thickness(10, 0, 0, 0); + Button defaults = MakeCompactButton("默认 9 个"); + defaults.Click += delegate { SelectDefaults(); }; + actions.Children.Add(defaults); + Button allA = MakeCompactButton("全部 CCF-A"); + allA.Margin = new Thickness(6, 0, 0, 0); + allA.Click += delegate { SelectAllCcfA(); }; + actions.Children.Add(allA); + Button robotics = MakeCompactButton("机器人 4 个"); + robotics.Margin = new Thickness(6, 0, 0, 0); + robotics.Click += delegate { SelectRoboticsFeatured(); }; + actions.Children.Add(robotics); + Button clear = MakeCompactButton("清空"); + clear.Margin = new Thickness(6, 0, 0, 0); + clear.Click += delegate { ClearSelection(); }; + actions.Children.Add(clear); + Grid.SetColumn(actions, 1); + toolbar.Children.Add(actions); + return toolbar; + } + + private void BuildConferenceRows( + StackPanel list, + List definitions, + HashSet initialSelection) + { + IEnumerable> groups = definitions + .OrderBy(delegate(ConferenceDefinition item) + { + return ConferenceCatalog.CategoryOrder(item.CategoryCode); + }) + .ThenBy(delegate(ConferenceDefinition item) { return item.Name; }) + .GroupBy(delegate(ConferenceDefinition item) + { + return String.IsNullOrWhiteSpace(item.CategoryCode) ? "MX" : item.CategoryCode; + }); + + foreach (IGrouping group in groups) + { + TextBlock category = MakeText( + ConferenceCatalog.CategoryDisplayName(group.Key) + " · " + group.Key, + 11, + FontWeights.SemiBold, + "#FF7890A7"); + category.Margin = new Thickness(4, rows.Count == 0 ? 4 : 14, 0, 7); + list.Children.Add(category); + SelectionGroup selectionGroup = new SelectionGroup(); + selectionGroup.Header = category; + selectionGroup.Rows = new List(); + this.groups.Add(selectionGroup); + + foreach (ConferenceDefinition definition in group) + { + Border rowBorder = new Border(); + rowBorder.Background = BrushFrom("#FFF5F8FB"); + rowBorder.BorderBrush = BrushFrom("#FFDDE7F0"); + rowBorder.BorderThickness = new Thickness(1); + rowBorder.CornerRadius = new CornerRadius(11); + rowBorder.Padding = new Thickness(11, 8, 11, 8); + rowBorder.Margin = new Thickness(0, 0, 0, 6); + + CheckBox checkBox = new CheckBox(); + checkBox.IsChecked = initialSelection.Contains(definition.Name); + checkBox.VerticalContentAlignment = VerticalAlignment.Center; + checkBox.Foreground = BrushFrom("#FF334A62"); + checkBox.Checked += delegate { UpdateSelectedCount(); }; + checkBox.Unchecked += delegate { UpdateSelectedCount(); }; + + Grid content = new Grid(); + content.Margin = new Thickness(6, 0, 0, 0); + content.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + content.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + StackPanel text = new StackPanel(); + TextBlock name = MakeText(definition.Name, 13, FontWeights.SemiBold, "#FF334A62"); + TextBlock description = MakeText( + String.IsNullOrWhiteSpace(definition.Description) ? "CCFDDL 收录会议" : definition.Description, + 10, + FontWeights.Normal, + "#FF7B8FA6"); + description.Margin = new Thickness(0, 3, 8, 0); + description.TextTrimming = TextTrimming.CharacterEllipsis; + text.Children.Add(name); + text.Children.Add(description); + content.Children.Add(text); + + string rankText = "CCF-" + (String.IsNullOrWhiteSpace(definition.CcfRank) ? "?" : definition.CcfRank); + if (definition.IsOriginalDefault + && !String.Equals(definition.CcfRank, "A", StringComparison.OrdinalIgnoreCase)) + { + rankText = "默认 · " + rankText; + } + if (definition.IsRoboticsFeatured) + { + rankText = "机器人 · " + rankText; + } + Border badge = new Border(); + string badgeBackground = definition.IsRoboticsFeatured + ? "#FFEAF6F0" + : (String.Equals(definition.CcfRank, "A", StringComparison.OrdinalIgnoreCase) + ? "#FFDDF4F7" + : "#FFF4F0F8"); + string badgeBorder = definition.IsRoboticsFeatured + ? "#FFC5E2D4" + : (String.Equals(definition.CcfRank, "A", StringComparison.OrdinalIgnoreCase) + ? "#FFB1DEE5" + : "#FFE1D7ED"); + string badgeForeground = definition.IsRoboticsFeatured ? "#FF4E806A" : "#FF58758B"; + badge.Background = BrushFrom(badgeBackground); + badge.BorderBrush = BrushFrom(badgeBorder); + badge.BorderThickness = new Thickness(1); + badge.CornerRadius = new CornerRadius(8); + badge.Padding = new Thickness(7, 3, 7, 3); + badge.VerticalAlignment = VerticalAlignment.Center; + badge.Child = MakeText(rankText, 9, FontWeights.SemiBold, badgeForeground); + Grid.SetColumn(badge, 1); + content.Children.Add(badge); + + checkBox.Content = content; + rowBorder.Child = checkBox; + list.Children.Add(rowBorder); + + SelectionRow row = new SelectionRow(); + row.Definition = definition; + row.Container = rowBorder; + row.CheckBox = checkBox; + row.SearchText = ( + definition.Name + " " + + definition.Description + " " + + definition.CategoryCode + " " + + ConferenceCatalog.CategoryDisplayName(definition.CategoryCode)) + .ToUpperInvariant(); + rows.Add(row); + selectionGroup.Rows.Add(row); + preservedUnknownSelections.Remove(definition.Name); + } + } + } + + private void ApplySearch() + { + if (searchPlaceholder != null) + { + searchPlaceholder.Visibility = searchBox.Text.Length == 0 + ? Visibility.Visible + : Visibility.Collapsed; + } + string query = searchBox.Text.Trim().ToUpperInvariant(); + for (int i = 0; i < rows.Count; i++) + { + rows[i].Container.Visibility = query.Length == 0 || rows[i].SearchText.Contains(query) + ? Visibility.Visible + : Visibility.Collapsed; + } + for (int i = 0; i < groups.Count; i++) + { + groups[i].Header.Visibility = groups[i].Rows.Any( + delegate(SelectionRow row) { return row.Container.Visibility == Visibility.Visible; }) + ? Visibility.Visible + : Visibility.Collapsed; + } + } + + private void SelectDefaults() + { + preservedUnknownSelections.Clear(); + for (int i = 0; i < rows.Count; i++) + { + rows[i].CheckBox.IsChecked = ConferenceCatalog.IsDefault(rows[i].Definition.Name); + } + UpdateSelectedCount(); + } + + private void SelectAllCcfA() + { + preservedUnknownSelections.Clear(); + for (int i = 0; i < rows.Count; i++) + { + rows[i].CheckBox.IsChecked = String.Equals( + rows[i].Definition.CcfRank, + "A", + StringComparison.OrdinalIgnoreCase); + } + UpdateSelectedCount(); + } + + private void SelectRoboticsFeatured() + { + preservedUnknownSelections.Clear(); + for (int i = 0; i < rows.Count; i++) + { + rows[i].CheckBox.IsChecked = rows[i].Definition.IsRoboticsFeatured; + } + UpdateSelectedCount(); + } + + private void ClearSelection() + { + preservedUnknownSelections.Clear(); + for (int i = 0; i < rows.Count; i++) + { + rows[i].CheckBox.IsChecked = false; + } + UpdateSelectedCount(); + } + + private void UpdateSelectedCount() + { + if (selectedCountText == null) + { + return; + } + int count = rows.Count(delegate(SelectionRow row) { return row.CheckBox.IsChecked == true; }); + selectedCountText.Text = "已选择 " + count.ToString() + " 个会议"; + } + + private void SaveSelection(object sender, RoutedEventArgs args) + { + SelectedNames = new HashSet(preservedUnknownSelections, StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < rows.Count; i++) + { + if (rows[i].CheckBox.IsChecked == true) + { + SelectedNames.Add(rows[i].Definition.Name); + } + } + DialogResult = true; + } + + private static Button MakeCompactButton(string text) + { + Button button = new Button(); + button.Content = text; + button.Height = 34; + button.Padding = new Thickness(9, 0, 9, 0); + button.Background = BrushFrom("#FFF0F5FA"); + button.BorderBrush = BrushFrom("#FFD5E2EE"); + button.BorderThickness = new Thickness(1); + button.Foreground = BrushFrom("#FF52677D"); + button.FontFamily = new FontFamily("Microsoft YaHei UI"); + button.FontSize = 10; + button.Cursor = Cursors.Hand; + return button; + } + + private static Button MakeActionButton(string text, bool primary) + { + Button button = MakeCompactButton(text); + button.MinWidth = primary ? 88 : 64; + button.Height = 38; + if (primary) + { + button.Background = BrushFrom("#FFDDF4F7"); + button.BorderBrush = BrushFrom("#FF9FD9E3"); + button.Foreground = BrushFrom("#FF16869D"); + button.FontWeight = FontWeights.SemiBold; + } + return button; + } + + private static Button MakeIconButton(string text, string tooltip) + { + Button button = MakeCompactButton(text); + button.Width = 32; + button.Padding = new Thickness(0); + button.FontSize = 16; + button.ToolTip = tooltip; + return button; + } + + private static TextBlock MakeText(string text, double size, FontWeight weight, string color) + { + TextBlock block = new TextBlock(); + block.Text = text; + block.FontFamily = new FontFamily("Microsoft YaHei UI"); + block.FontSize = size; + block.FontWeight = weight; + block.Foreground = BrushFrom(color); + return block; + } + + private static Brush BrushFrom(string value) + { + Brush brush = (Brush)new BrushConverter().ConvertFromString(value); + if (brush.CanFreeze) + { + brush.Freeze(); + } + return brush; + } + + private sealed class SelectionRow + { + internal ConferenceDefinition Definition; + internal Border Container; + internal CheckBox CheckBox; + internal string SearchText; + } + + private sealed class SelectionGroup + { + internal TextBlock Header; + internal List Rows; + } + } +} diff --git a/MainWindow.cs b/MainWindow.cs index df668d8..80a29af 100644 --- a/MainWindow.cs +++ b/MainWindow.cs @@ -30,11 +30,15 @@ internal sealed class MainWindow : Window private readonly TextBlock statusText; private readonly Grid footer; private readonly WinForms.NotifyIcon trayIcon; + private readonly ConferencePreferences preferences; private TextBlock refreshGlyph; + private TextBlock subtitleText; private Button topmostButton; private WinForms.ToolStripMenuItem topmostTrayItem; private List currentItems; + private List allItems; + private List availableConferences; private DockEdge dockEdge; private Rect dockWorkArea; private bool isTucked; @@ -46,11 +50,22 @@ internal sealed class MainWindow : Window private int cornerDragHandleCount; internal MainWindow() + : this(false) + { + } + + internal MainWindow(bool useTransientDefaultPreferences) { dataService = new ConferenceDataService(); + preferences = useTransientDefaultPreferences + ? ConferencePreferences.CreateTransientDefaults() + : ConferencePreferences.Load(); cardViews = new Dictionary(StringComparer.OrdinalIgnoreCase); currentItems = new List(); + allItems = new List(); + availableConferences = ConferenceCatalog.CreateDefaultDefinitions(); dockEdge = DockEdge.None; + isBehaviorTest = useTransientDefaultPreferences; Title = "Conference DDL"; Width = 372; @@ -172,7 +187,7 @@ private Grid BuildHeader() titlePanel.MouseLeftButtonDown += BeginWindowDrag; TextBlock titleText = MakeText("Conference DDL", 20, FontWeights.Bold, "#FF2B3E55"); - TextBlock subtitleText = MakeText("9 个会议 · Full paper 投稿截止", 11, FontWeights.Normal, "#FF768BA3"); + subtitleText = MakeText("9 个会议 · Full paper 投稿截止", 11, FontWeights.Normal, "#FF768BA3"); subtitleText.Margin = new Thickness(0, 3, 0, 0); titlePanel.Children.Add(titleText); titlePanel.Children.Add(subtitleText); @@ -188,6 +203,10 @@ private Grid BuildHeader() refreshButton.Click += async delegate { await RefreshDataAsync(); }; buttons.Children.Add(refreshButton); + Button settingsButton = MakeHeaderButton("⚙", "选择显示的会议"); + settingsButton.Click += delegate { OpenConferenceSettings(); }; + buttons.Children.Add(settingsButton); + topmostButton = MakeHeaderButton("置顶", "取消始终置顶"); topmostButton.Width = 46; topmostButton.Click += delegate { SetTopmost(!Topmost); }; @@ -308,8 +327,9 @@ private async System.Threading.Tasks.Task RefreshDataAsync() try { DataLoadResult result = await dataService.LoadAsync(DateTimeOffset.Now); - currentItems = OrderItems(result.Items); - RebuildCards(); + allItems = result.Items; + availableConferences = result.Conferences; + ApplyConferenceSelection(); string offlineMark = result.IsOffline ? " · 离线" : String.Empty; statusText.Text = result.SourceLabel + offlineMark + " · " + result.LoadedAt.ToString("HH:mm"); } @@ -324,6 +344,36 @@ private async System.Threading.Tasks.Task RefreshDataAsync() } } + private void OpenConferenceSettings() + { + StopAutoHide(); + ConferenceSelectionWindow dialog = new ConferenceSelectionWindow( + availableConferences, + preferences.SelectedNames); + dialog.Owner = this; + dialog.Topmost = Topmost; + bool? result = dialog.ShowDialog(); + if (result == true && dialog.SelectedNames != null) + { + preferences.ReplaceSelected(dialog.SelectedNames); + preferences.Save(); + ApplyConferenceSelection(); + } + ArmAutoHide(); + } + + private void ApplyConferenceSelection() + { + currentItems = OrderItems( + allItems.Where(delegate(ConferenceDisplayItem item) + { + return preferences.SelectedNames.Contains(item.Series); + }).ToList()); + subtitleText.Text = currentItems.Count.ToString(CultureInfo.InvariantCulture) + + " 个会议 · Full paper 投稿截止"; + RebuildCards(); + } + private static List OrderItems(List items) { return items @@ -338,14 +388,15 @@ private static List OrderItems(List CreateDefaultDefinitions() + { + List definitions = new List(); + for (int i = 0; i < DefaultNames.Length; i++) + { + string name = DefaultNames[i]; + ConferenceDefinition definition = new ConferenceDefinition(); + definition.Name = name; + definition.Description = DefaultDescription(name); + definition.CategoryCode = DefaultCategory(name); + definition.CcfRank = DefaultRank(name); + definition.IsOriginalDefault = true; + definitions.Add(definition); + } + return definitions; + } + + internal static int CategoryOrder(string categoryCode) + { + string[] codes = { "RB", "AI", "CG", "DB", "DS", "SC", "SE", "NW", "HI", "CT", "MX" }; + for (int i = 0; i < codes.Length; i++) + { + if (String.Equals(codes[i], categoryCode, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + return Int32.MaxValue; + } + + internal static string CategoryDisplayName(string categoryCode) + { + string code = String.IsNullOrWhiteSpace(categoryCode) ? "MX" : categoryCode.ToUpperInvariant(); + switch (code) + { + case "RB": return "机器人精选"; + case "AI": return "人工智能"; + case "CG": return "图形学与多媒体"; + case "DB": return "数据库、数据挖掘与检索"; + case "DS": return "体系结构、并行与存储"; + case "SC": return "网络与信息安全"; + case "SE": return "软件工程、系统与编程语言"; + case "NW": return "计算机网络"; + case "HI": return "人机交互"; + case "CT": return "计算机科学理论"; + case "MX": return "交叉与综合"; + default: return code; + } + } + + private static string DefaultDescription(string name) + { + switch (name) + { + case "NeurIPS": return "Neural Information Processing Systems"; + case "ICML": return "International Conference on Machine Learning"; + case "ICLR": return "International Conference on Learning Representations"; + case "CVPR": return "Computer Vision and Pattern Recognition"; + case "AAAI": return "AAAI Conference on Artificial Intelligence"; + case "ICCV": return "International Conference on Computer Vision"; + case "ECCV": return "European Conference on Computer Vision"; + case "ACM SIGGRAPH": return "ACM SIGGRAPH Annual Conference"; + case "ACM SIGGRAPH ASIA": return "ACM SIGGRAPH Annual Conference in Asia"; + default: return name; + } + } + + private static string DefaultCategory(string name) + { + if (name == "ACM SIGGRAPH" || name == "ACM SIGGRAPH ASIA") + { + return "CG"; + } + return "AI"; + } + + private static string DefaultRank(string name) + { + if (name == "ECCV") + { + return "B"; + } + if (name == "ACM SIGGRAPH ASIA") + { + return "N"; + } + return "A"; + } + } + + internal sealed class ConferenceDefinition + { + internal string Name; + internal string Description; + internal string CategoryCode; + internal string CcfRank; + internal bool IsOriginalDefault; + internal bool IsRoboticsFeatured; } internal sealed class DeadlineValue @@ -51,6 +176,9 @@ internal ConferenceEdition() } internal string Series; + internal string Description; + internal string CategoryCode; + internal string CcfRank; internal int Year; internal string Website; internal string Timezone; @@ -84,9 +212,11 @@ internal sealed class DataLoadResult internal DataLoadResult() { Items = new List(); + Conferences = new List(); } internal List Items; + internal List Conferences; internal string SourceLabel; internal DateTime LoadedAt; internal bool IsOffline; diff --git a/Program.cs b/Program.cs index 185297a..0705a25 100644 --- a/Program.cs +++ b/Program.cs @@ -38,11 +38,15 @@ private static void Main(string[] args) Application application = new Application(); application.ShutdownMode = ShutdownMode.OnExplicitShutdown; - MainWindow window = new MainWindow(); + bool isWindowSelfTest = args != null && args.Any( + delegate(string argument) + { + return String.Equals(argument, "--window-self-test", StringComparison.OrdinalIgnoreCase); + }); + MainWindow window = new MainWindow(isWindowSelfTest); application.MainWindow = window; - if (args != null && args.Length >= 1 - && String.Equals(args[0], "--window-self-test", StringComparison.OrdinalIgnoreCase)) + if (isWindowSelfTest) { window.ContentRendered += async delegate { @@ -85,7 +89,7 @@ private static void Main(string[] args) return; } - Console.WriteLine("PASS: sorting, four-corner dragging, outside-release snap, edge tuck, and reveal animation."); + Console.WriteLine("PASS: selection picker, sorting, four-corner dragging, outside-release snap, edge tuck, and reveal animation."); Environment.ExitCode = 0; application.Shutdown(); }; @@ -93,6 +97,21 @@ private static void Main(string[] args) window.Show(); + if (args != null && args.Length >= 2 + && String.Equals(args[0], "--render-settings-preview", StringComparison.OrdinalIgnoreCase)) + { + window.ContentRendered += async delegate + { + await Task.Delay(5000); + Window settingsWindow = window.CreateConferenceSelectionWindowForTest(); + settingsWindow.Show(); + await Task.Delay(700); + RenderWindow(settingsWindow, args[1]); + settingsWindow.Close(); + application.Shutdown(); + }; + } + if (args != null && args.Length >= 2 && String.Equals(args[0], "--render-preview", StringComparison.OrdinalIgnoreCase)) { diff --git a/README.md b/README.md index a7bf80e..acad944 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,33 @@

一个轻量、安静的 Windows 学术会议投稿截止浮窗。
- 只看 full-paper deadline,只显示到天,把注意力留给论文。 + 自选 CCF-A、机器人与常用会议,只看 full-paper deadline,只显示到天。

## 预览

- Conference DDL 浮窗截图 + Conference DDL 浮窗截图 + Conference DDL 会议选择设置

## 为什么做它 -浏览器里的会议日历很完整,但不一定适合每天扫一眼。Conference DDL 常驻桌面一角,自动把已公布的投稿截止排在前面,并按剩余天数递增;尚未公布下一届日期的会议安静地留在列表底部。 +浏览器里的会议日历很完整,但不一定适合每天扫一眼。Conference DDL 常驻桌面一角,自动把已公布的投稿截止排在前面,并按剩余天数递增;尚未公布下一届日期的会议安静地留在列表底部。设置页可以搜索并勾选上游收录的 CCF-A 与机器人精选会议,不需要把不相关的方向塞进浮窗。 -## 支持的会议 +## 会议范围 -| AI / ML | Vision | Graphics | -| --- | --- | --- | -| NeurIPS · ICML · ICLR · AAAI | CVPR · ICCV · ECCV | SIGGRAPH · SIGGRAPH Asia | +默认仍显示最初的 9 个会议:NeurIPS、ICML、ICLR、CVPR、AAAI、ICCV、ECCV、SIGGRAPH 与 SIGGRAPH Asia。 + +点击右上角 ⚙ 后,还可以选择 CCFDDL 当前有数据的全部 CCF-A 会议,覆盖人工智能、图形学、数据库、体系结构、安全、软件工程、网络、人机交互、理论与交叉方向。会议池根据上游 `rank.ccf` 标记动态生成;ECCV 与 SIGGRAPH Asia 虽不是上游 CCF-A,仍作为原始默认项保留。 + +考虑到 CCF 分类对机器人领域覆盖不完整,设置页另设“机器人精选”分组:ICRA、IROS、RSS、CoRL。项目只维护这四个会议的身份名单;年份、官网、full-paper deadline 与时区仍来自 CCFDDL,不另建容易过期的日期表。上游没有对应数据时,该会议不会凭空生成日期。 ## 功能 - 只读取 full-paper `deadline`,不会把摘要截止误当成投稿截止 +- 搜索、按领域分组并多选会议;支持“默认 9 个”“全部 CCF-A”“机器人 4 个”和清空 +- 选择结果保存在本机,下次启动自动恢复 - 天级倒计时:`还剩 55 天` / `不足 1 天`,没有秒级跳动 - 已公布且未截止的会议置顶,并按截止时间从近到远排列 - 靠近屏幕边缘自动吸附,停留约 1.5 秒后平滑收起;鼠标靠近再滑出 @@ -45,9 +50,9 @@ 1. 在 [Releases](https://github.com/wenan4/ConferenceDDL-Windows/releases/latest) 下载 `ConferenceDDL.exe` 或 Windows 压缩包。 2. 双击运行。Windows 首次运行下载的独立 EXE 时可能显示安全提示,请确认文件来自本仓库后继续。 -3. 右上角可刷新、切换置顶或隐藏;隐藏后可从系统托盘重新打开。 +3. 右上角可选择会议、刷新、切换置顶或隐藏;隐藏后可从系统托盘重新打开。 -程序会在 `%LOCALAPPDATA%\ConferenceDDL` 保存一份会议数据缓存。删除该目录即可清除缓存,不会影响程序本体。 +程序会在 `%LOCALAPPDATA%\ConferenceDeadlineWidget` 保存会议数据缓存与选择设置。删除该目录即可恢复初始状态,不会影响程序本体。 ## 从源码构建 diff --git a/app.manifest b/app.manifest index 2ed0ff1..e669e9b 100644 --- a/app.manifest +++ b/app.manifest @@ -1,6 +1,6 @@ - + diff --git a/assets/screenshots/settings.png b/assets/screenshots/settings.png new file mode 100644 index 0000000..6fe4f16 Binary files /dev/null and b/assets/screenshots/settings.png differ diff --git a/assets/screenshots/widget.png b/assets/screenshots/widget.png index 2c3d7a5..1a19f61 100644 Binary files a/assets/screenshots/widget.png and b/assets/screenshots/widget.png differ