Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

steps:
- name: Check out source
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Build test executable
shell: powershell
Expand All @@ -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: |
Expand Down
4 changes: 2 additions & 2 deletions AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
221 changes: 192 additions & 29 deletions ConferenceDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,47 @@ internal async Task<DataLoadResult> LoadAsync(DateTimeOffset now)
if (!String.IsNullOrWhiteSpace(remoteData))
{
List<ConferenceEdition> editions = ParseYaml(remoteData);
List<ConferenceDisplayItem> 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<ConferenceDisplayItem> 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<ConferenceDisplayItem> fallbackItems = SelectItems(ParseYaml(FallbackYaml), now);
return CreateResult(fallbackItems, "内置备用数据", true);
List<ConferenceEdition> fallbackEditions = ParseYaml(FallbackYaml);
return TryCreateResult(fallbackEditions, now, "内置备用数据", true);
}

private static DataLoadResult CreateResult(
List<ConferenceDisplayItem> items,
private static DataLoadResult TryCreateResult(
List<ConferenceEdition> editions,
DateTimeOffset now,
string sourceLabel,
bool isOffline)
{
List<ConferenceDefinition> 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;
Expand All @@ -78,7 +90,7 @@ private static async Task<string> 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++)
{
Expand Down Expand Up @@ -147,9 +159,11 @@ internal static List<ConferenceEdition> 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');
Expand All @@ -168,25 +182,46 @@ internal static List<ConferenceEdition> 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;
Expand Down Expand Up @@ -268,7 +303,7 @@ internal static List<ConferenceEdition> ParseYaml(string yaml)
}
}

AddEdition(editions, currentEdition, targetBlock);
AddEdition(editions, currentEdition);
return editions;
}

Expand All @@ -285,10 +320,9 @@ private static int CountIndent(string line)

private static void AddEdition(
List<ConferenceEdition> 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);
}
Expand Down Expand Up @@ -326,15 +360,84 @@ private static string Unquote(string value)
return result;
}

internal static List<ConferenceDefinition> GetSelectableConferences(
List<ConferenceEdition> editions)
{
Dictionary<string, ConferenceDefinition> definitions =
new Dictionary<string, ConferenceDefinition>(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<ConferenceDefinition> 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<ConferenceDisplayItem> SelectItems(
List<ConferenceEdition> editions,
DateTimeOffset now)
{
return SelectItems(editions, ConferenceCatalog.DefaultNames, now);
}

internal static List<ConferenceDisplayItem> SelectItems(
List<ConferenceEdition> editions,
IEnumerable<string> names,
DateTimeOffset now)
{
List<ConferenceDisplayItem> items = new List<ConferenceDisplayItem>();

for (int nameIndex = 0; nameIndex < ConferenceCatalog.OrderedNames.Length; nameIndex++)
foreach (string name in names.Distinct(StringComparer.OrdinalIgnoreCase))
{
string name = ConferenceCatalog.OrderedNames[nameIndex];
List<ConferenceEdition> matching = editions
.Where(delegate(ConferenceEdition edition)
{
Expand Down Expand Up @@ -517,7 +620,7 @@ internal static int RunSelfTest()
List<ConferenceEdition> parsed = ParseYaml(FallbackYaml);
List<ConferenceDisplayItem> selected = SelectItems(parsed, now);

if (selected.Count != 9)
if (selected.Count != ConferenceCatalog.DefaultNames.Length)
{
throw new InvalidOperationException("Expected nine conference rows.");
}
Expand Down Expand Up @@ -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<ConferenceDefinition> 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)
Expand All @@ -565,12 +719,21 @@ internal static int RunFileTest(string yaml, DateTimeOffset now)
try
{
List<ConferenceEdition> parsed = ParseYaml(yaml);
List<ConferenceDisplayItem> selected = SelectItems(parsed, now);
if (selected.Count != ConferenceCatalog.OrderedNames.Length)
{
throw new InvalidOperationException("Expected nine conference rows.");
List<ConferenceDefinition> conferences = GetSelectableConferences(parsed);
List<ConferenceDisplayItem> 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];
Expand Down
Loading