Skip to content

Commit 5201f60

Browse files
committed
feat: OneNote sync, Outlook Sticky Notes, local profiles, widget desktop fix
- OneNote sync via Microsoft Graph API (OAuth2 PKCE, notebook/section management) - Pull Sticky Notes from Outlook via /me/mailFolders/notes/messages - Import local Sticky Notes from Windows SQLite database (plum.sqlite) - Local profiles system: create, rename, delete profiles with separate note storage - Cloud providers are mutually exclusive (switching disconnects others) - Sync button with rotation animation during sync - Note count displayed in title bar - Window state (open/position) saved per profile/provider context - Widget: ignore desktop window click in fullscreen detection - Version bump to 0.5.0
1 parent 99af084 commit 5201f60

12 files changed

Lines changed: 1855 additions & 71 deletions

‎ActionPanel.cs‎

Lines changed: 138 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ public static Flyout CreateSettings(string currentTheme, string currentBackdropT
192192
Action onChangeFolder, Action onResetFolder,
193193
Action onConfigureFirebase, Action onDisconnectFirebase, Action onSyncFirebase,
194194
Action onConfigureWebDav, Action onDisconnectWebDav, Action onSyncWebDav,
195+
bool isOneNoteConnected = false, string? oneNoteUser = null,
196+
string? activeProfile = null, List<string>? profiles = null,
197+
Action<string>? onSwitchProfile = null, Action? onCreateProfile = null,
198+
Action<string, Action>? onRenameProfile = null, Action<string>? onDeleteProfile = null,
199+
Func<string>? getActiveProfile = null, Func<List<string>>? getProfiles = null,
200+
Action? onConfigureOneNote = null, Action? onDisconnectOneNote = null, Action? onSyncOneNote = null,
195201
Action<Flyout>? onShowVoiceModels = null,
196202
Action<Flyout>? onShowShortcuts = null,
197203
string? currentLanguage = null, bool slashEnabled = true,
@@ -304,17 +310,27 @@ static string FindLabel((string key, string label)[] items, string current)
304310
var isCustomFolder = !string.Equals(currentNotesFolder, defaultNotesFolder, StringComparison.OrdinalIgnoreCase);
305311
var storageSubMenu = CreateStorageSubMenu(
306312
isCustomFolder, isFirebaseConnected, isWebDavConnected,
313+
isOneNoteConnected,
307314
currentNotesFolder, firebaseEmail, webDavUrl,
315+
oneNoteUser,
316+
activeProfile ?? "", profiles ?? [],
308317
onResetFolder, onChangeFolder,
318+
onSwitchProfile ?? (_ => { }), onCreateProfile ?? (() => { }),
319+
onRenameProfile ?? ((_, _) => { }), onDeleteProfile ?? (_ => { }),
309320
onConfigureFirebase, onDisconnectFirebase, onSyncFirebase,
310321
onConfigureWebDav, onDisconnectWebDav, onSyncWebDav,
322+
onConfigureOneNote ?? (() => { }), onDisconnectOneNote ?? (() => { }), onSyncOneNote ?? (() => { }),
323+
getActiveProfile ?? (() => activeProfile ?? ""),
324+
getProfiles ?? (() => profiles ?? []),
311325
() => flyout.Hide());
312-
var storageValue = isFirebaseConnected ? Lang.T("cloud")
326+
var storageValue = isFirebaseConnected ? "Firebase"
313327
: isWebDavConnected ? "WebDAV"
328+
: isOneNoteConnected ? "OneNote"
329+
: !string.IsNullOrEmpty(activeProfile) ? activeProfile
314330
: isCustomFolder ? Lang.T("custom_folder")
315331
: Lang.T("local");
316332
var storageBtn = CreateCascadeButton(Lang.T("storage"), storageValue, storageSubMenu, "\uE753");
317-
storageBtn.Tag = Lang.T("storage") + " Cloud Firebase WebDAV Local";
333+
storageBtn.Tag = Lang.T("storage") + " Firebase WebDAV OneNote Local";
318334
allButtons.Add(storageBtn);
319335
panel.Children.Add(storageBtn);
320336

@@ -476,57 +492,146 @@ private static Flyout CreateRadioSubMenu(string groupName,
476492

477493
private static Flyout CreateStorageSubMenu(
478494
bool isCustomFolder, bool isFirebaseConnected, bool isWebDavConnected,
495+
bool isOneNoteConnected,
479496
string currentNotesFolder, string? firebaseEmail, string? webDavUrl,
497+
string? oneNoteUser,
498+
string activeProfile, List<string> profiles,
480499
Action onResetFolder, Action onChangeFolder,
500+
Action<string> onSwitchProfile, Action onCreateProfile,
501+
Action<string, Action> onRenameProfile, Action<string> onDeleteProfile,
481502
Action onConfigureFirebase, Action onDisconnectFirebase, Action onSyncFirebase,
482503
Action onConfigureWebDav, Action onDisconnectWebDav, Action onSyncWebDav,
504+
Action onConfigureOneNote, Action onDisconnectOneNote, Action onSyncOneNote,
505+
Func<string> getActiveProfile, Func<List<string>> getProfiles,
483506
Action onDone)
484507
{
485508
var flyout = new Flyout
486509
{
487510
Placement = Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.RightEdgeAlignedTop,
488-
FlyoutPresenterStyle = CreateFlyoutPresenterStyle(200, 300)
511+
FlyoutPresenterStyle = CreateFlyoutPresenterStyle(210, 360)
489512
};
490513

491514
var panel = new StackPanel { Spacing = 0 };
492515

493-
bool isLocal = !isFirebaseConnected && !isWebDavConnected && !isCustomFolder;
494-
panel.Children.Add(CreateCheckItem(Lang.T("local"), isLocal, () =>
516+
void RebuildPanel()
495517
{
496-
if (isCustomFolder) onResetFolder(); else if (isFirebaseConnected) onDisconnectFirebase();
497-
onDone();
498-
}));
518+
panel.Children.Clear();
519+
var currentProfiles = getProfiles();
520+
var currentActive = getActiveProfile();
521+
bool cloudConnected = isFirebaseConnected || isWebDavConnected || isOneNoteConnected;
499522

500-
var folderLabel = isCustomFolder ? $"{Lang.T("custom_folder")} — {ShortenPath(currentNotesFolder)}" : Lang.T("custom_folder");
501-
panel.Children.Add(CreateCheckItem(folderLabel, isCustomFolder && !isFirebaseConnected, () =>
502-
{
503-
onChangeFolder(); onDone();
504-
}));
523+
// ── Local (default) ──
524+
bool defaultLocal = !cloudConnected && !isCustomFolder && string.IsNullOrEmpty(currentActive);
525+
panel.Children.Add(CreateCheckItem(Lang.T("local"), defaultLocal, () =>
526+
{
527+
onSwitchProfile("");
528+
onDone();
529+
}));
505530

506-
panel.Children.Add(CreateSeparator());
531+
// ── Profiles ──
532+
foreach (var p in currentProfiles)
533+
{
534+
var name = p;
535+
bool active = !cloudConnected && string.Equals(currentActive, name, StringComparison.OrdinalIgnoreCase);
536+
var profileBtn = CreateCheckItem(name, active, () =>
537+
{
538+
onSwitchProfile(name);
539+
onDone();
540+
});
507541

508-
if (isFirebaseConnected)
509-
{
510-
var cloudLabel = !string.IsNullOrEmpty(firebaseEmail) ? $"{Lang.T("cloud")} — {firebaseEmail}" : Lang.T("cloud");
511-
panel.Children.Add(CreateCheckItem(cloudLabel, true, () => { onSyncFirebase(); onDone(); }));
512-
panel.Children.Add(CreateActionItem(Lang.T("disconnect"), () => { onDisconnectFirebase(); onDone(); }));
513-
}
514-
else
515-
{
516-
panel.Children.Add(CreateCheckItem(Lang.T("cloud"), false, () => { onConfigureFirebase(); onDone(); }));
517-
}
542+
var ctxFlyout = new Flyout
543+
{
544+
Placement = Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.RightEdgeAlignedTop,
545+
FlyoutPresenterStyle = CreateFlyoutPresenterStyle(140, 80)
546+
};
547+
var ctxPanel = new StackPanel { Spacing = 0 };
548+
ctxPanel.Children.Add(CreateActionItem(Lang.T("rename"), () =>
549+
{
550+
ctxFlyout.Hide();
551+
onRenameProfile(name, RebuildPanel);
552+
}));
518553

519-
if (isWebDavConnected)
520-
{
521-
var wdLabel = !string.IsNullOrEmpty(webDavUrl) ? $"WebDAV — {ShortenPath(webDavUrl)}" : "WebDAV";
522-
panel.Children.Add(CreateCheckItem(wdLabel, true, () => { onSyncWebDav(); onDone(); }));
523-
panel.Children.Add(CreateActionItem(Lang.T("disconnect"), () => { onDisconnectWebDav(); onDone(); }));
524-
}
525-
else
526-
{
527-
panel.Children.Add(CreateCheckItem("WebDAV", false, () => { onConfigureWebDav(); onDone(); }));
554+
var deleteBtn = new Button
555+
{
556+
HorizontalAlignment = HorizontalAlignment.Stretch,
557+
HorizontalContentAlignment = HorizontalAlignment.Stretch,
558+
Background = new SolidColorBrush(Microsoft.UI.Colors.Transparent),
559+
BorderThickness = new Thickness(0),
560+
Padding = new Thickness(8, 4, 8, 4),
561+
CornerRadius = new CornerRadius(4),
562+
MinHeight = 0,
563+
Content = new TextBlock
564+
{
565+
Text = Lang.T("delete"),
566+
FontSize = 12,
567+
Foreground = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 235, 70, 70))
568+
}
569+
};
570+
deleteBtn.Click += (_, _) =>
571+
{
572+
ctxFlyout.Hide();
573+
onDeleteProfile(name);
574+
RebuildPanel();
575+
};
576+
ctxPanel.Children.Add(deleteBtn);
577+
578+
ctxFlyout.Content = ctxPanel;
579+
profileBtn.ContextFlyout = ctxFlyout;
580+
panel.Children.Add(profileBtn);
581+
}
582+
583+
// ── New profile button ──
584+
panel.Children.Add(CreateActionItem("+ " + Lang.T("new_profile"), () => { onCreateProfile(); onDone(); }));
585+
586+
panel.Children.Add(CreateSeparator());
587+
588+
// ── Custom folder ──
589+
var folderLabel = isCustomFolder ? $"{Lang.T("custom_folder")} — {ShortenPath(currentNotesFolder)}" : Lang.T("custom_folder");
590+
panel.Children.Add(CreateCheckItem(folderLabel, isCustomFolder && !cloudConnected, () =>
591+
{
592+
onChangeFolder(); onDone();
593+
}));
594+
595+
panel.Children.Add(CreateSeparator());
596+
597+
// ── Firebase ──
598+
if (isFirebaseConnected)
599+
{
600+
var fbLabel = !string.IsNullOrEmpty(firebaseEmail) ? $"Firebase — {firebaseEmail}" : "Firebase";
601+
panel.Children.Add(CreateCheckItem(fbLabel, true, () => { onSyncFirebase(); onDone(); }));
602+
panel.Children.Add(CreateActionItem(Lang.T("disconnect"), () => { onDisconnectFirebase(); onDone(); }));
603+
}
604+
else
605+
{
606+
panel.Children.Add(CreateCheckItem("Firebase", false, () => { onConfigureFirebase(); onDone(); }));
607+
}
608+
609+
// ── WebDAV ──
610+
if (isWebDavConnected)
611+
{
612+
var wdLabel = !string.IsNullOrEmpty(webDavUrl) ? $"WebDAV — {ShortenPath(webDavUrl)}" : "WebDAV";
613+
panel.Children.Add(CreateCheckItem(wdLabel, true, () => { onSyncWebDav(); onDone(); }));
614+
panel.Children.Add(CreateActionItem(Lang.T("disconnect"), () => { onDisconnectWebDav(); onDone(); }));
615+
}
616+
else
617+
{
618+
panel.Children.Add(CreateCheckItem("WebDAV", false, () => { onConfigureWebDav(); onDone(); }));
619+
}
620+
621+
// ── OneNote ──
622+
if (isOneNoteConnected)
623+
{
624+
var onLabel = !string.IsNullOrEmpty(oneNoteUser) ? $"OneNote — {oneNoteUser}" : "OneNote";
625+
panel.Children.Add(CreateCheckItem(onLabel, true, () => { onSyncOneNote(); onDone(); }));
626+
panel.Children.Add(CreateActionItem(Lang.T("disconnect"), () => { onDisconnectOneNote(); onDone(); }));
627+
}
628+
else
629+
{
630+
panel.Children.Add(CreateCheckItem("OneNote", false, () => { onConfigureOneNote(); onDone(); }));
631+
}
528632
}
529633

634+
RebuildPanel();
530635
flyout.Content = panel;
531636
return flyout;
532637
}

‎AppSettings.cs‎

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,65 @@ public static void SaveNotesFolder(string folder)
326326
MergeAndSaveSettings(new Dictionary<string, object> { ["notesFolder"] = folder });
327327
}
328328

329+
// ── Local profiles ────────────────────────────────────────
330+
331+
public static string LoadActiveProfile()
332+
{
333+
try
334+
{
335+
if (File.Exists(SettingsPath))
336+
{
337+
var json = File.ReadAllText(SettingsPath);
338+
var doc = JsonDocument.Parse(json);
339+
if (doc.RootElement.TryGetProperty("activeProfile", out var prop))
340+
return prop.GetString() ?? "";
341+
}
342+
}
343+
catch { }
344+
return "";
345+
}
346+
347+
public static void SaveActiveProfile(string name)
348+
{
349+
MergeAndSaveSettings(new Dictionary<string, object> { ["activeProfile"] = name });
350+
}
351+
352+
public static List<string> LoadProfiles()
353+
{
354+
var profiles = new List<string>();
355+
try
356+
{
357+
var dir = Path.Combine(SettingsDir, "profiles");
358+
if (Directory.Exists(dir))
359+
{
360+
foreach (var file in Directory.GetFiles(dir, "*.json"))
361+
profiles.Add(Path.GetFileNameWithoutExtension(file));
362+
}
363+
}
364+
catch { }
365+
profiles.Sort(StringComparer.OrdinalIgnoreCase);
366+
return profiles;
367+
}
368+
369+
public static string GetProfilePath(string name)
370+
{
371+
var dir = Path.Combine(SettingsDir, "profiles");
372+
Directory.CreateDirectory(dir);
373+
return Path.Combine(dir, $"{name}.json");
374+
}
375+
376+
public static void DeleteProfile(string name)
377+
{
378+
try
379+
{
380+
var path = GetProfilePath(name);
381+
if (File.Exists(path)) File.Delete(path);
382+
if (LoadActiveProfile() == name)
383+
SaveActiveProfile("");
384+
}
385+
catch { }
386+
}
387+
329388
// ── Firebase ───────────────────────────────────────────────
330389

331390
public static (string Url, string ApiKey, string RefreshToken) LoadFirebaseSettings()
@@ -435,6 +494,36 @@ public static void SaveWebDavSettings(string url, string username, string passwo
435494
});
436495
}
437496

497+
// ── OneNote ────────────────────────────────────────────────
498+
499+
public static (string ClientId, string RefreshToken) LoadOneNoteSettings()
500+
{
501+
try
502+
{
503+
if (File.Exists(SettingsPath))
504+
{
505+
var json = File.ReadAllText(SettingsPath);
506+
var doc = JsonDocument.Parse(json);
507+
var clientId = doc.RootElement.TryGetProperty("onenoteClientId", out var cProp)
508+
? cProp.GetString() ?? "" : "";
509+
var token = doc.RootElement.TryGetProperty("onenoteRefreshToken", out var tProp)
510+
? tProp.GetString() ?? "" : "";
511+
return (clientId, token);
512+
}
513+
}
514+
catch { }
515+
return ("", "");
516+
}
517+
518+
public static void SaveOneNoteSettings(string clientId, string refreshToken = "")
519+
{
520+
MergeAndSaveSettings(new Dictionary<string, object>
521+
{
522+
["onenoteClientId"] = clientId,
523+
["onenoteRefreshToken"] = refreshToken
524+
});
525+
}
526+
438527
// ── Apply to any window ─────────────────────────────────────
439528

440529
public static void ApplyToWindow(Window window, BackdropSettings settings,
@@ -898,6 +987,11 @@ public static List<PersistedWindowState> LoadOpenWindows()
898987
}
899988

900989
public static void SaveOpenWindows(IEnumerable<PersistedWindowState> windows)
990+
{
991+
SaveOpenWindows(windows, "openWindows");
992+
}
993+
994+
public static void SaveOpenWindows(IEnumerable<PersistedWindowState> windows, string key)
901995
{
902996
var payload = windows.Select(w => new Dictionary<string, object>
903997
{
@@ -910,10 +1004,52 @@ public static void SaveOpenWindows(IEnumerable<PersistedWindowState> windows)
9101004

9111005
MergeAndSaveSettings(new Dictionary<string, object>
9121006
{
913-
["openWindows"] = payload
1007+
[key] = payload
9141008
});
9151009
}
9161010

1011+
public static List<PersistedWindowState> LoadOpenWindows(string key)
1012+
{
1013+
var result = new List<PersistedWindowState>();
1014+
try
1015+
{
1016+
if (!File.Exists(SettingsPath)) return result;
1017+
var json = File.ReadAllText(SettingsPath);
1018+
var doc = JsonDocument.Parse(json);
1019+
if (!doc.RootElement.TryGetProperty(key, out var windowsProp)) return result;
1020+
1021+
var arrayElement = windowsProp;
1022+
JsonDocument? nestedDoc = null;
1023+
if (windowsProp.ValueKind == JsonValueKind.String)
1024+
{
1025+
var raw = windowsProp.GetString();
1026+
if (!string.IsNullOrWhiteSpace(raw))
1027+
{
1028+
nestedDoc = JsonDocument.Parse(raw);
1029+
arrayElement = nestedDoc.RootElement;
1030+
}
1031+
}
1032+
1033+
if (arrayElement.ValueKind != JsonValueKind.Array) { nestedDoc?.Dispose(); return result; }
1034+
1035+
foreach (var item in arrayElement.EnumerateArray())
1036+
{
1037+
if (item.ValueKind != JsonValueKind.Object) continue;
1038+
var type = item.TryGetProperty("type", out var tp) ? tp.GetString() ?? "" : "";
1039+
var noteId = item.TryGetProperty("noteId", out var np) ? np.GetString() ?? "" : "";
1040+
if (!item.TryGetProperty("x", out var xp) || !item.TryGetProperty("y", out var yp)
1041+
|| !xp.TryGetInt32(out var x) || !yp.TryGetInt32(out var y)
1042+
|| string.IsNullOrWhiteSpace(type)) continue;
1043+
var isCompact = item.TryGetProperty("isCompact", out var cp)
1044+
&& cp.ValueKind is JsonValueKind.True or JsonValueKind.False && cp.GetBoolean();
1045+
result.Add(new PersistedWindowState(type, noteId, x, y, isCompact));
1046+
}
1047+
nestedDoc?.Dispose();
1048+
}
1049+
catch { }
1050+
return result;
1051+
}
1052+
9171053
private static void MergeAndSaveSettings(Dictionary<string, object> newValues)
9181054
{
9191055
try

0 commit comments

Comments
 (0)