diff --git a/.gitignore b/.gitignore index 117195c..14de929 100644 --- a/.gitignore +++ b/.gitignore @@ -351,6 +351,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ -#Flow launcher specific -release.ps1 -appveyor.yml +#VSCode +.vscode/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c957232 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "Odotocodot.OneNote.Linq"] + path = Odotocodot.OneNote.Linq + url = https://github.com/Odotocodot/Odotocodot.OneNote.Linq + branch = flow-plugin diff --git a/Constants.cs b/Constants.cs deleted file mode 100644 index 34b6270..0000000 --- a/Constants.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Flow.Launcher.Plugin.OneNote -{ - public static class Icons - { - - public const string Logo = "Images/logo.png"; - public const string Unavailable = "Images/unavailable.png"; - public const string Sync = "Images/refresh.png"; - public const string Recent = "Images/recent.png"; - public const string RecentPage = "Images/recent_page.png"; - public const string Warning = "Images/warning.png"; - - public const string Section = "Images/section.png"; - public const string Notebook = "Images/notebook.png"; - public const string NewPage = "Images/new_page.png"; - public const string NewSection = "Images/new_section.png"; - public const string NewNotebook = "Images/new_notebook.png"; - } - public static class Keywords - { - public const string NotebookExplorer = "nb:\\"; - public const string RecentPages = "rcntpgs:"; - } -} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote.sln b/Flow.Launcher.Plugin.OneNote.sln new file mode 100644 index 0000000..7c6236a --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33712.159 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.OneNote", "Flow.Launcher.Plugin.OneNote\Flow.Launcher.Plugin.OneNote.csproj", "{3801047C-BEF0-4774-91DB-B64EEE874BB2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{65CCED4F-8513-4345-A2A8-14970B836C5F}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + LICENSE = LICENSE + Readme.md = Readme.md + release.ps1 = release.ps1 + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Odotocodot.OneNote.Linq", "Odotocodot.OneNote.Linq\Odotocodot.OneNote.Linq\Odotocodot.OneNote.Linq.csproj", "{AB2CEDD9-15DB-4EAF-A675-10928E83918D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3801047C-BEF0-4774-91DB-B64EEE874BB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3801047C-BEF0-4774-91DB-B64EEE874BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3801047C-BEF0-4774-91DB-B64EEE874BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3801047C-BEF0-4774-91DB-B64EEE874BB2}.Release|Any CPU.Build.0 = Release|Any CPU + {AB2CEDD9-15DB-4EAF-A675-10928E83918D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB2CEDD9-15DB-4EAF-A675-10928E83918D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB2CEDD9-15DB-4EAF-A675-10928E83918D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB2CEDD9-15DB-4EAF-A675-10928E83918D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CD75E0C8-C471-42DD-803A-C428D4B7622A} + EndGlobalSection +EndGlobal diff --git a/Flow.Launcher.Plugin.OneNote.csproj b/Flow.Launcher.Plugin.OneNote/Flow.Launcher.Plugin.OneNote.csproj similarity index 67% rename from Flow.Launcher.Plugin.OneNote.csproj rename to Flow.Launcher.Plugin.OneNote/Flow.Launcher.Plugin.OneNote.csproj index f283a9e..d40de89 100644 --- a/Flow.Launcher.Plugin.OneNote.csproj +++ b/Flow.Launcher.Plugin.OneNote/Flow.Launcher.Plugin.OneNote.csproj @@ -1,7 +1,7 @@ - + - net5.0-windows + net7.0-windows Flow.Launcher.Plugin.OneNote Flow.Launcher.Plugin.OneNote Odotocodot @@ -9,6 +9,8 @@ https://github.com/Odotocodot/Flow.Launcher.Plugin.OneNote flow-launcher flow-plugin false + true + true @@ -23,10 +25,9 @@ - - - - + + + @@ -35,5 +36,7 @@ - + + + diff --git a/Flow.Launcher.Plugin.OneNote/Icons.cs b/Flow.Launcher.Plugin.OneNote/Icons.cs new file mode 100644 index 0000000..e118d92 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/Icons.cs @@ -0,0 +1,123 @@ +using Odotocodot.OneNote.Linq; +using System; +using System.IO; + +namespace Flow.Launcher.Plugin.OneNote +{ + public class Icons : BaseModel + { + public const string Logo = "Images/logo.png"; + public const string Sync = "Images/refresh.png"; + public const string Warning = "Images/warning.png"; + public const string Search = Logo; + public const string RecycleBin = "Images/recycle_bin.png"; + public const string Recent = "Images/recent.png"; + public const string RecentPage = "Images/recent_page.png"; + + public const string Page = Logo; + public const string Section = "Images/section.png"; + public const string SectionGroup = "Images/section_group.png"; + public const string Notebook = "Images/notebook.png"; + + public const string NewPage = "Images/new_page.png"; + public const string NewSection = "Images/new_section.png"; + public const string NewSectionGroup = "Images/new_section_group.png"; + public const string NewNotebook = "Images/new_notebook.png"; + + private OneNoteItemIcons notebookIcons; + private OneNoteItemIcons sectionIcons; + private Settings settings; + + public int CachedIconCount => notebookIcons.IconCount + sectionIcons.IconCount; + public string CachedIconsFileSize => GetBytesReadable(notebookIcons.IconsFileSize + sectionIcons.IconsFileSize); + public static string NotebookIconDirectory { get; private set; } + public static string SectionIconDirectory { get; private set; } + + + private static readonly Lazy lazy = new(); + public static Icons Instance => lazy.Value; + + public static void Init(PluginInitContext context, Settings settings) + { + NotebookIconDirectory = Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "Images", "NotebookIcons"); + SectionIconDirectory = Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "Images", "SectionIcons"); + + Instance.notebookIcons = new OneNoteItemIcons(NotebookIconDirectory, Path.Combine(context.CurrentPluginMetadata.PluginDirectory, Notebook)); + Instance.sectionIcons = new OneNoteItemIcons(SectionIconDirectory, Path.Combine(context.CurrentPluginMetadata.PluginDirectory, Section)); + + + Instance.notebookIcons.PropertyChanged += Instance.IconCountChanged; + Instance.sectionIcons.PropertyChanged += Instance.IconCountChanged; + + Instance.settings = settings; + } + + public static void Close() + { + Instance.notebookIcons.PropertyChanged -= Instance.IconCountChanged; + Instance.sectionIcons.PropertyChanged -= Instance.IconCountChanged; + } + private void IconCountChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + OnPropertyChanged(nameof(CachedIconCount)); + OnPropertyChanged(nameof(CachedIconsFileSize)); + } + + public static string GetIcon(IOneNoteItem item) + { + return item switch + { + OneNoteNotebook notebook => Instance.settings.CreateColoredIcons && notebook.Color.HasValue + ? Instance.notebookIcons.GetIcon(notebook.Color.Value) + : Notebook, + OneNoteSectionGroup sectionGroup => sectionGroup.IsRecycleBin + ? RecycleBin + : SectionGroup, + OneNoteSection section => Instance.settings.CreateColoredIcons && section.Color.HasValue + ? Instance.sectionIcons.GetIcon(section.Color.Value) + : Section, + OneNotePage => Page, + _ => Warning, + }; + } + + public void ClearCachedIcons() + { + notebookIcons.ClearCachedIcons(); + sectionIcons.ClearCachedIcons(); + } + + // Returns the human-readable file size for an arbitrary, 64-bit file size + // The default format is "0.### XB", e.g. "4.2 KB" or "1.434 GB" + private static string GetBytesReadable(long i) + { + // Get absolute value + long absolute_i = Math.Abs(i); + // Determine the suffix and readable value + string suffix; + double readable; + switch (absolute_i) + { + case >= 0x40000000: // Gigabyte + suffix = "GB"; + readable = i >> 20; + break; + case >= 0x100000: // Megabyte + suffix = "MB"; + readable = i >> 10; + break; + case >= 0x400: + suffix = "KB"; // Kilobyte + readable = i; + break; + default: + return i.ToString("0 B"); // Byte + } + // Divide by 1024 to get fractional value + readable /= 1024; + // Return formatted number with suffix + return readable.ToString("0.## ") + suffix; + } + + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Images/logo.png b/Flow.Launcher.Plugin.OneNote/Images/logo.png new file mode 100644 index 0000000..ea9fb28 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/logo.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/new_notebook.png b/Flow.Launcher.Plugin.OneNote/Images/new_notebook.png new file mode 100644 index 0000000..6eef320 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/new_notebook.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/new_page.png b/Flow.Launcher.Plugin.OneNote/Images/new_page.png new file mode 100644 index 0000000..aee4a9b Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/new_page.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/new_section.png b/Flow.Launcher.Plugin.OneNote/Images/new_section.png new file mode 100644 index 0000000..0e46a95 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/new_section.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/new_section_group.png b/Flow.Launcher.Plugin.OneNote/Images/new_section_group.png new file mode 100644 index 0000000..7219366 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/new_section_group.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/notebook.png b/Flow.Launcher.Plugin.OneNote/Images/notebook.png new file mode 100644 index 0000000..b9f296d Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/notebook.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/recent.png b/Flow.Launcher.Plugin.OneNote/Images/recent.png new file mode 100644 index 0000000..ee97336 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/recent.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/recent_page.png b/Flow.Launcher.Plugin.OneNote/Images/recent_page.png new file mode 100644 index 0000000..9f7a5de Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/recent_page.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/recycle_bin.png b/Flow.Launcher.Plugin.OneNote/Images/recycle_bin.png new file mode 100644 index 0000000..879d0e0 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/recycle_bin.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/refresh.png b/Flow.Launcher.Plugin.OneNote/Images/refresh.png new file mode 100644 index 0000000..98e70e3 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/refresh.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/section.png b/Flow.Launcher.Plugin.OneNote/Images/section.png new file mode 100644 index 0000000..e336883 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/section.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/section_group.png b/Flow.Launcher.Plugin.OneNote/Images/section_group.png new file mode 100644 index 0000000..9859d55 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/section_group.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Images/warning.png b/Flow.Launcher.Plugin.OneNote/Images/warning.png new file mode 100644 index 0000000..da4f218 Binary files /dev/null and b/Flow.Launcher.Plugin.OneNote/Images/warning.png differ diff --git a/Flow.Launcher.Plugin.OneNote/Keywords.cs b/Flow.Launcher.Plugin.OneNote/Keywords.cs new file mode 100644 index 0000000..540831f --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/Keywords.cs @@ -0,0 +1,11 @@ +namespace Flow.Launcher.Plugin.OneNote +{ + public class Keywords + { + public const string NotebookExplorerSeparator = "\\"; + public string NotebookExplorer { get; set; } = $"nb:{NotebookExplorerSeparator}"; + public string RecentPages { get; set; } = "rcntpgs:"; + public string TitleSearch { get; set; } = "*"; + public string ScopedSearch { get; set; } = ">"; + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/Main.cs b/Flow.Launcher.Plugin.OneNote/Main.cs new file mode 100644 index 0000000..1e4f456 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/Main.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Odotocodot.OneNote.Linq; +namespace Flow.Launcher.Plugin.OneNote +{ + public class Main : IAsyncPlugin, IContextMenu, ISettingProvider, IDisposable + { + private PluginInitContext context; + + private SearchManager searchManager; + private Settings settings; + + private static SemaphoreSlim semaphore; + public Task InitAsync(PluginInitContext context) + { + this.context = context; + settings = context.API.LoadSettingJsonStorage(); + Icons.Init(context, settings); + searchManager = new SearchManager(context, settings, new ResultCreator(context, settings)); + semaphore = new SemaphoreSlim(1,1); + context.API.VisibilityChanged += OnVisibilityChanged; + return Task.CompletedTask; + } + + public void OnVisibilityChanged(object _, VisibilityChangedEventArgs e) + { + if (context.CurrentPluginMetadata.Disabled || !e.IsVisible) + { + OneNoteApplication.ReleaseComObject(); + } + } + + private static async Task OneNoteInitAsync(CancellationToken token = default) + { + if (semaphore.CurrentCount == 0 || OneNoteApplication.HasComObject) + return; + + await semaphore.WaitAsync(token); + OneNoteApplication.Init(); + semaphore.Release(); + } + public async Task> QueryAsync(Query query, CancellationToken token) + { + var init = OneNoteInitAsync(token); + + if (string.IsNullOrEmpty(query.Search)) + return searchManager.EmptyQuery(); + + await init; + + return query.FirstSearch switch + { + string fs when fs.StartsWith(settings.Keywords.RecentPages) => searchManager.RecentPages(fs), + string fs when fs.StartsWith(settings.Keywords.NotebookExplorer) => searchManager.NotebookExplorer(query), + string fs when fs.StartsWith(settings.Keywords.TitleSearch) => searchManager.TitleSearch(string.Join(' ', query.SearchTerms), OneNoteApplication.GetNotebooks()), + _ => searchManager.DefaultSearch(query.Search) + }; + } + + public List LoadContextMenus(Result selectedResult) + { + return searchManager.ContextMenu(selectedResult); + } + + public System.Windows.Controls.Control CreateSettingPanel() + { + return new UI.Views.SettingsView(new UI.ViewModels.SettingsViewModel(context, settings)); + } + + public void Dispose() + { + context.API.VisibilityChanged -= OnVisibilityChanged; + semaphore.Dispose(); + Icons.Close(); + OneNoteApplication.ReleaseComObject(); + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/OneNoteItemIcons.cs b/Flow.Launcher.Plugin.OneNote/OneNoteItemIcons.cs new file mode 100644 index 0000000..a4994da --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/OneNoteItemIcons.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Flow.Launcher.Plugin.OneNote +{ + public class OneNoteItemIcons : BaseModel + { + private readonly Dictionary icons; + private readonly string iconDirectory; + private readonly string baseIconPath; + + public OneNoteItemIcons(string folderPath, string baseIconPath) + { + icons = new Dictionary(); + iconDirectory = folderPath; + this.baseIconPath = baseIconPath; + + Directory.CreateDirectory(iconDirectory); + foreach (var imagePath in Directory.EnumerateFiles(iconDirectory)) + { + if (int.TryParse(Path.GetFileNameWithoutExtension(imagePath), out int argb)) + icons.Add(Color.FromArgb(argb), imagePath); + } + } + public int IconCount => icons.Count; + + public long IconsFileSize => new DirectoryInfo(iconDirectory).EnumerateFiles() + .Select(file => file.Length) + .Aggregate(0L, (a, b) => a + b); + public void ClearCachedIcons() + { + icons.Clear(); + foreach (var img in new DirectoryInfo(iconDirectory).EnumerateFiles()) + { + img.Delete(); + } + OnPropertyChanged(nameof(IconCount)); + OnPropertyChanged(nameof(IconsFileSize)); + } + + public string GetIcon(Color color) + { + if (!icons.TryGetValue(color, out string path)) + { + //Create Colored Image + using var bitmap = new Bitmap(baseIconPath); + BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat); + + int bytesPerPixel = Bitmap.GetPixelFormatSize(bitmap.PixelFormat) / 8; + byte[] pixels = new byte[bitmapData.Stride * bitmap.Height]; + IntPtr pointer = bitmapData.Scan0; + Marshal.Copy(pointer, pixels, 0, pixels.Length); + int bytesWidth = bitmapData.Width * bytesPerPixel; + + for (int j = 0; j < bitmapData.Height; j++) + { + int line = j * bitmapData.Stride; + for (int i = 0; i < bytesWidth; i += bytesPerPixel) + { + pixels[line + i] = color.B; + pixels[line + i + 1] = color.G; + pixels[line + i + 2] = color.R; + } + } + + Marshal.Copy(pixels, 0, pointer, pixels.Length); + bitmap.UnlockBits(bitmapData); + path = Path.Combine(iconDirectory, color.ToArgb() + ".png"); + bitmap.Save(path, ImageFormat.Png); + + icons.Add(color, path); + OnPropertyChanged(nameof(IconCount)); + OnPropertyChanged(nameof(IconsFileSize)); + + } + + return path; + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/ResultCreator.cs b/Flow.Launcher.Plugin.OneNote/ResultCreator.cs new file mode 100644 index 0000000..4bf9b70 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/ResultCreator.cs @@ -0,0 +1,267 @@ +using System.Collections.Generic; +using System.Linq; +using Odotocodot.OneNote.Linq; + +namespace Flow.Launcher.Plugin.OneNote +{ + public class ResultCreator + { + private readonly PluginInitContext context; + private readonly Settings settings; + + private const string PathSeparator = " > "; + + public ResultCreator(PluginInitContext context, Settings settings) + { + this.settings = settings; + this.context = context; + } + + private static string GetNicePath(IOneNoteItem item, bool includeSelf = true, string separator = PathSeparator) + { + return item.RelativePath.Replace(OneNoteParser.RelativePathSeparator.ToString(), separator); + } + + private string GetTitle(IOneNoteItem item, List highlightData) + { + string title = item.Name; + if (item.IsUnread && settings.ShowUnread) + { + string unread = "\u2022 "; + title = title.Insert(0, unread); + + if (highlightData != null) + { + for (int i = 0; i < highlightData.Count; i++) + { + highlightData[i] += unread.Length; + } + } + } + return title; + } + + #region Create OneNote Item Results + public Result CreatePageResult(OneNotePage page, string query = null) + { + return CreateOneNoteItemResult(page, false, string.IsNullOrWhiteSpace(query) ? null : context.API.FuzzySearch(query, page.Name).MatchData); + } + + public Result CreateOneNoteItemResult(IOneNoteItem item, bool actionIsAutoComplete, List highlightData = null, int score = 0) + { + string title = GetTitle(item, highlightData); + string titleToolTip = null; + string subTitle = GetNicePath(item, true); + string subTitleToolTip = null; + string autoCompleteText = $"{context.CurrentPluginMetadata.ActionKeyword} {settings.Keywords.NotebookExplorer}{GetNicePath(item, true, Keywords.NotebookExplorerSeparator)}"; + + switch (item) + { + case OneNoteNotebook notebook: + titleToolTip = $"{notebook.Name}\n\n" + + $"Last Modified:\t{notebook.LastModified:F}\n" + + $"Sections:\t\t{notebook.Sections.Count()}\n" + + $"Sections Groups:\t{notebook.SectionGroups.Count()}"; + + subTitle = string.Empty; + autoCompleteText += Keywords.NotebookExplorerSeparator; + break; + case OneNoteSectionGroup sectionGroup: + subTitleToolTip = $"{subTitle}\n\n" + + $"Last Modified:\t{sectionGroup.LastModified:F}\n" + + $"Sections:\t\t{sectionGroup.Sections.Count()}\n" + + $"Sections Groups:\t{sectionGroup.SectionGroups.Count()}"; + + autoCompleteText += Keywords.NotebookExplorerSeparator; + break; + case OneNoteSection section: + if (section.Encrypted) + { + title += " [Encrypted]"; + if (section.Locked) + title += "[Locked]"; + else + title += "[Unlocked]"; + } + + subTitleToolTip = $"{subTitle}\n\n" + + $"Last Modified:\t{section.LastModified}\n" + + $"Pages:\t\t{section.Pages.Count()}"; + + autoCompleteText += Keywords.NotebookExplorerSeparator; + break; + case OneNotePage page: + actionIsAutoComplete = false; + subTitle = subTitle.Remove(subTitle.Length - (page.Name.Length + PathSeparator.Length)); + subTitleToolTip = $"{subTitle}\n\n" + + $"Created:\t\t{page.Created:F}\n" + + $"Last Modified:\t{page.LastModified:F}"; + break; + } + return new Result + { + Title = title, + TitleToolTip = titleToolTip, + TitleHighlightData = highlightData, + SubTitle = subTitle, + SubTitleToolTip = subTitleToolTip, + AutoCompleteText = autoCompleteText, + Score = score, + IcoPath = Icons.GetIcon(item), + ContextData = item, + Action = c => + { + if (actionIsAutoComplete) + { + context.API.ChangeQuery(autoCompleteText); + return false; + } + OneNoteApplication.SyncItem(item); + OneNoteApplication.OpenInOneNote(item); + return true; + }, + }; + } + #endregion + + #region Create New OneNote Item Results + public static Result CreateNewPageResult(string pageTitle, OneNoteSection section) + { + pageTitle = pageTitle.Trim(); + return new Result + { + Title = $"Create page: \"{pageTitle}\"", + SubTitle = $"Path: {GetNicePath(section, true)} > {pageTitle}", + IcoPath = Icons.NewPage, + Action = c => + { + OneNoteApplication.CreatePage(section, pageTitle); + return true; + } + }; + } + + public Result CreateNewSectionResult(string sectionTitle, IOneNoteItem parent) + { + sectionTitle = sectionTitle.Trim(); + bool validTitle = OneNoteParser.IsSectionNameValid(sectionTitle); + + return new Result + { + Title = $"Create section: \"{sectionTitle}\"", + SubTitle = validTitle + ? $"Path: {GetNicePath(parent, true)} > {sectionTitle}" + : $"Section names cannot contain: {string.Join(' ', OneNoteParser.InvalidSectionChars)}", + IcoPath = Icons.NewSection, + Action = c => + { + if (!validTitle) + return false; + + switch (parent) + { + case OneNoteNotebook notebook: + OneNoteApplication.CreateSection(notebook, sectionTitle); + break; + case OneNoteSectionGroup sectionGroup: + OneNoteApplication.CreateSection(sectionGroup, sectionTitle); + break; + default: + break; + } + + context.API.ChangeQuery(context.CurrentPluginMetadata.ActionKeyword); + return true; + + } + }; + } + public Result CreateNewSectionGroupResult(string sectionGroupTitle, IOneNoteItem parent) + { + sectionGroupTitle = sectionGroupTitle.Trim(); + bool validTitle = OneNoteParser.IsSectionGroupNameValid(sectionGroupTitle); + + return new Result + { + Title = $"Create section group: \"{sectionGroupTitle}\"", + SubTitle = validTitle + ? $"Path: {GetNicePath(parent, true)} > {sectionGroupTitle}" + : $"Section group names cannot contain: {string.Join(' ', OneNoteParser.InvalidSectionGroupChars)}", + IcoPath = Icons.NewSectionGroup, + Action = c => + { + if (!validTitle) + return false; + + switch (parent) + { + case OneNoteNotebook notebook: + OneNoteApplication.CreateSectionGroup(notebook, sectionGroupTitle); + break; + case OneNoteSectionGroup sectionGroup: + OneNoteApplication.CreateSectionGroup(sectionGroup, sectionGroupTitle); + break; + default: + break; + } + + context.API.ChangeQuery(context.CurrentPluginMetadata.ActionKeyword); + return true; + } + }; + } + + public Result CreateNewNotebookResult(string notebookTitle) + { + notebookTitle = notebookTitle.Trim(); + bool validTitle = OneNoteParser.IsNotebookNameValid(notebookTitle); + + return new Result + { + Title = $"Create notebook: \"{notebookTitle}\"", + SubTitle = validTitle + ? $"Location: {OneNoteApplication.GetDefaultNotebookLocation()}" + : $"Notebook names cannot contain: {string.Join(' ', OneNoteParser.InvalidNotebookChars)}", + IcoPath = Icons.NewNotebook, + Action = c => + { + if (!validTitle) + return false; + + OneNoteApplication.CreateNotebook(notebookTitle); + + context.API.ChangeQuery(context.CurrentPluginMetadata.ActionKeyword); + return true; + } + }; + } + + #endregion + + public static List NoMatchesFoundResult() + { + return SingleResult("No matches found", + "Try searching something else, or syncing your notebooks.", + Icons.Logo); + } + public static List InvalidQuery() + { + return SingleResult("Invalid query", + "The first character of the search must be a letter or a digit", + Icons.Warning); + } + + public static List SingleResult(string title, string subTitle, string iconPath) + { + return new List + { + new Result + { + Title = title, + SubTitle = subTitle, + IcoPath = iconPath, + } + }; + } + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/SearchManager.cs b/Flow.Launcher.Plugin.OneNote/SearchManager.cs new file mode 100644 index 0000000..1862b25 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/SearchManager.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Odotocodot.OneNote.Linq; + +namespace Flow.Launcher.Plugin.OneNote +{ + public class SearchManager + { + private readonly PluginInitContext context; + private readonly Settings settings; + private readonly ResultCreator rc; + + public SearchManager(PluginInitContext context, Settings settings, ResultCreator resultCreator) + { + this.context = context; + this.settings = settings; + rc = resultCreator; + } + #region Notebook Explorer + public List NotebookExplorer(Query query) + { + var results = new List(); + + string fullSearch = query.Search.Remove(query.Search.IndexOf(settings.Keywords.NotebookExplorer), settings.Keywords.NotebookExplorer.Length); + + IOneNoteItem parent = null; + IEnumerable collection = OneNoteApplication.GetNotebooks(); + + string[] searches = fullSearch.Split(Keywords.NotebookExplorerSeparator, StringSplitOptions.None); + + for (int i = -1; i < searches.Length - 1; i++) + { + if (i < 0) + continue; + + parent = collection.FirstOrDefault(item => item.Name == searches[i]); + if (parent == null) + return results; + + collection = parent.Children; + } + + string lastSearch = searches[^1]; + + results = lastSearch switch + { + //Empty search so show all in collection + string ls when string.IsNullOrWhiteSpace(ls) + => NotebookEmptySearch(parent, collection), + + //Search by title + string ls when ls.StartsWith(settings.Keywords.TitleSearch) && parent is not OneNotePage + => TitleSearch(ls, collection, parent), + + //scoped search + string ls when ls.StartsWith(settings.Keywords.ScopedSearch) && (parent is OneNoteNotebook || parent is OneNoteSectionGroup) + => ScopedSearch(ls, parent), + + //default search + _ => NotebookDefaultSearch(parent, collection, lastSearch) + }; + + if (parent != null) + { + var result = rc.CreateOneNoteItemResult(parent, false, score: 4000); + result.Title = $"Open \"{parent.Name}\" in OneNote"; + result.SubTitle = lastSearch switch + { + string ls when ls.StartsWith(settings.Keywords.TitleSearch) + => $"Now search by title in \"{parent.Name}\"", + + string ls when ls.StartsWith(settings.Keywords.ScopedSearch) + => $"Now searching all pages in \"{parent.Name}\"", + + _ => $"Use \'{settings.Keywords.ScopedSearch}\' to search this item. Use \'{settings.Keywords.TitleSearch}\' to search by title in this item", + }; + + results.Add(result); + } + + return results; + } + + private List NotebookDefaultSearch(IOneNoteItem parent, IEnumerable collection, string lastSearch) + { + List highlightData = null; + int score = 0; + + var results = collection.Where(SettingsCheck) + .Where(item => FuzzySearch(item.Name, lastSearch, out highlightData, out score)) + .Select(item => rc.CreateOneNoteItemResult(item, true, highlightData, score)) + .ToList(); + + AddCreateNewOneNoteItemResults(results, parent, lastSearch); + return results; + } + + private List NotebookEmptySearch(IOneNoteItem parent, IEnumerable collection) + { + List results = collection.Where(SettingsCheck) + .Select(item => rc.CreateOneNoteItemResult(item, true)) + .ToList(); + if (!results.Any()) + { + switch (parent) //parent can be null if the collection contains notebooks. + { + case OneNoteNotebook: + case OneNoteSectionGroup: + //can create section/section group + results.Add(NoItemsInCollectionResult("section", Icons.NewSection, "(unencrypted) section")); + results.Add(NoItemsInCollectionResult("section group", Icons.NewSectionGroup)); + break; + case OneNoteSection section: + //can create page + if (!section.Locked) + results.Add(NoItemsInCollectionResult("page", Icons.NewPage)); + break; + default: + break; + } + } + + return results; + + static Result NoItemsInCollectionResult(string title, string iconPath, string subTitle = null) + { + return new Result + { + Title = $"Create {title}: \"\"", + SubTitle = $"No {subTitle ?? title}s found. Type a valid title to create one", + IcoPath = iconPath, + }; + } + } + + private List ScopedSearch(string query, IOneNoteItem parent) + { + if (query.Length == settings.Keywords.ScopedSearch.Length) + return ResultCreator.NoMatchesFoundResult(); + + if (!char.IsLetterOrDigit(query[settings.Keywords.ScopedSearch.Length])) + return ResultCreator.InvalidQuery(); + + string currentSearch = query[settings.Keywords.TitleSearch.Length..]; + var results = new List(); + + results = OneNoteApplication.FindPages(parent, currentSearch) + .Select(pg => rc.CreatePageResult(pg, currentSearch)) + .ToList(); + + if (!results.Any()) + results = ResultCreator.NoMatchesFoundResult(); + + return results; + } + + private void AddCreateNewOneNoteItemResults(List results, IOneNoteItem parent, string query) + { + if (!results.Any(result => string.Equals(query.Trim(), result.Title, StringComparison.OrdinalIgnoreCase))) + { + if (parent?.IsInRecycleBin() == true) + return; + + switch (parent) + { + case null: + results.Add(rc.CreateNewNotebookResult(query)); + break; + case OneNoteNotebook: + case OneNoteSectionGroup: + results.Add(rc.CreateNewSectionResult(query, parent)); + results.Add(rc.CreateNewSectionGroupResult(query, parent)); + break; + case OneNoteSection section: + if (!section.Locked) + results.Add(ResultCreator.CreateNewPageResult(query, section)); + break; + default: + break; + } + } + } + #endregion + + public List EmptyQuery() + { + return new List + { + new Result + { + Title = "Search OneNote pages", + SubTitle = $"Type \"{settings.Keywords.NotebookExplorer}\" or select this option to search by notebook structure ", + AutoCompleteText = $"{context.CurrentPluginMetadata.ActionKeyword} {settings.Keywords.NotebookExplorer}", + IcoPath = Icons.Logo, + Score = 2000, + Action = c => + { + context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeyword} {settings.Keywords.NotebookExplorer}"); + return false; + }, + }, + new Result + { + Title = "See recent pages", + SubTitle = $"Type \"{settings.Keywords.RecentPages}\" or select this option to see recently modified pages", + AutoCompleteText = $"{context.CurrentPluginMetadata.ActionKeyword} {settings.Keywords.RecentPages}", + IcoPath = Icons.Recent, + Score = -1000, + Action = c => + { + context.API.ChangeQuery($"{context.CurrentPluginMetadata.ActionKeyword} {settings.Keywords.RecentPages}"); + return false; + }, + }, + new Result + { + Title = "New quick note", + IcoPath = Icons.NewPage, + Score = -4000, + Action = c => + { + OneNoteApplication.CreateQuickNote(); + return true; + } + }, + new Result + { + Title = "Open and sync notebooks", + IcoPath = Icons.Sync, + Score = int.MinValue, + Action = c => + { + foreach (var notebook in OneNoteApplication.GetNotebooks()) + { + notebook.Sync(); + } + OneNoteApplication.GetNotebooks() + .GetPages() + .OrderByDescending(pg => pg.LastModified) + .First() + .OpenInOneNote(); + return true; + } + }, + }; + } + public List DefaultSearch(string query) + { + //Check for invalid start of query i.e. symbols + if (!char.IsLetterOrDigit(query[0])) + return ResultCreator.InvalidQuery(); + + var results = OneNoteApplication.FindPages(query) + .Select(pg => rc.CreatePageResult(pg, query)); + if (results.Any()) + return results.ToList(); + + return ResultCreator.NoMatchesFoundResult(); + } + public List TitleSearch(string query, IEnumerable currentCollection, IOneNoteItem parent = null) + { + if (query.Length == settings.Keywords.TitleSearch.Length && parent == null) + return ResultCreator.SingleResult($"Now searching by title.", null, Icons.Search); + + List highlightData = null; + int score = 0; + + var currentSearch = query[settings.Keywords.TitleSearch.Length..]; + + var results = currentCollection.Traverse(item => + { + if (!SettingsCheck(item)) + return false; + + return FuzzySearch(item.Name, currentSearch, out highlightData, out score); + }) + .Select(item => rc.CreateOneNoteItemResult(item, false, highlightData, score)) + .ToList(); + + if (!results.Any()) + results = ResultCreator.NoMatchesFoundResult(); + + return results; + } + public List RecentPages(string query) + { + int count = settings.DefaultRecentsCount; + if (query.Length > settings.Keywords.RecentPages.Length && int.TryParse(query[settings.Keywords.RecentPages.Length..], out int userChosenCount)) + count = userChosenCount; + + return OneNoteApplication.GetNotebooks() + .GetPages() + .Where(SettingsCheck) + .OrderByDescending(pg => pg.LastModified) + .Take(count) + .Select(pg => + { + Result result = rc.CreatePageResult(pg); + result.SubTitleToolTip = result.SubTitle; + result.SubTitle = $"{GetLastEdited(DateTime.Now - pg.LastModified)}\t{result.SubTitle}"; + result.IcoPath = Icons.RecentPage; + return result; + }) + .ToList(); + } + public List ContextMenu(Result selectedResult) + { + var results = new List(); + if (selectedResult.ContextData is IOneNoteItem item) + { + var result = rc.CreateOneNoteItemResult(item, false); + result.Title = $"Open and sync \"{item.Name}\""; + result.SubTitle = string.Empty; + result.ContextData = null; + results.Add(result); + } + return results; + } + private static string GetLastEdited(TimeSpan diff) + { + string lastEdited = "Last edited "; + if (PluralCheck(diff.TotalDays, "day", ref lastEdited) + || PluralCheck(diff.TotalHours, "hour", ref lastEdited) + || PluralCheck(diff.TotalMinutes, "min", ref lastEdited) + || PluralCheck(diff.TotalSeconds, "sec", ref lastEdited)) + return lastEdited; + else + return lastEdited += "Now."; + + static bool PluralCheck(double totalTime, string timeType, ref string lastEdited) + { + var roundedTime = (int)Math.Round(totalTime); + if (roundedTime > 0) + { + string plural = roundedTime == 1 ? "" : "s"; + lastEdited += $"{roundedTime} {timeType}{plural} ago."; + return true; + } + else + return false; + + } + } + private bool FuzzySearch(string itemName, string search, out List highlightData, out int score) + { + var matchResult = context.API.FuzzySearch(search, itemName); + highlightData = matchResult.MatchData; + score = matchResult.Score; + return matchResult.IsSearchPrecisionScoreMet(); + } + private bool SettingsCheck(IOneNoteItem item) + { + bool success = true; + if (!settings.ShowEncrypted && item is OneNoteSection section) + success = !section.Encrypted; + + if (!settings.ShowRecycleBin && item.IsInRecycleBin()) + success = false; + return success; + } + + } + +} diff --git a/Flow.Launcher.Plugin.OneNote/Settings.cs b/Flow.Launcher.Plugin.OneNote/Settings.cs new file mode 100644 index 0000000..ba823fc --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/Settings.cs @@ -0,0 +1,38 @@ +namespace Flow.Launcher.Plugin.OneNote +{ + public class Settings : UI.Model + { + private bool showUnread = true; + private int defaultRecentsCount = 5; + private bool showRecycleBin = true; + private bool showEncrypted = false; + private bool createColoredIcons = true; + public Keywords Keywords { get; init; } = new Keywords(); + + public bool ShowRecycleBin + { + get => showRecycleBin; + set => SetProperty(ref showRecycleBin, value); + } + public bool ShowUnread + { + get => showUnread; + set => SetProperty(ref showUnread, value); + } + public int DefaultRecentsCount + { + get => defaultRecentsCount; + set => SetProperty(ref defaultRecentsCount, value); + } + public bool ShowEncrypted + { + get => showEncrypted; + set => SetProperty(ref showEncrypted, value); + } + public bool CreateColoredIcons + { + get => createColoredIcons; + set => SetProperty(ref createColoredIcons, value); + } + } +} diff --git a/Flow.Launcher.Plugin.OneNote/UI/Model.cs b/Flow.Launcher.Plugin.OneNote/UI/Model.cs new file mode 100644 index 0000000..1d9845b --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/Model.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; + +namespace Flow.Launcher.Plugin.OneNote.UI +{ + public class Model : BaseModel + { + protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string propertyName = null) + { + if (Equals(field, newValue)) + return false; + + field = newValue; + OnPropertyChanged(propertyName); + return true; + } + } +} diff --git a/Flow.Launcher.Plugin.OneNote/UI/Styles.xaml b/Flow.Launcher.Plugin.OneNote/UI/Styles.xaml new file mode 100644 index 0000000..f9903bf --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/Styles.xaml @@ -0,0 +1,236 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/UI/ViewModels/ChangeKeywordViewModel.cs b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/ChangeKeywordViewModel.cs new file mode 100644 index 0000000..e4f4ca8 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/ChangeKeywordViewModel.cs @@ -0,0 +1,52 @@ +using System.Linq; + +namespace Flow.Launcher.Plugin.OneNote.UI.ViewModels +{ + public class ChangeKeywordViewModel : Model + { + private readonly PluginInitContext context; + private readonly KeywordViewModel[] keywords; + private string newKeyword; + + public ChangeKeywordViewModel(SettingsViewModel settingsViewModel) + { + context = settingsViewModel.context; + keywords = settingsViewModel.Keywords; + SelectedKeyword = settingsViewModel.SelectedKeyword; + } + public KeywordViewModel SelectedKeyword { get; init; } + public string NewKeyword { get => newKeyword; set => SetProperty(ref newKeyword, value); } + + public bool ChangeKeyword(out string errorMessage) + { + errorMessage = null; + var oldKeyword = SelectedKeyword.Keyword; + if (string.IsNullOrWhiteSpace(NewKeyword)) + { + errorMessage = "The new keyword cannot be empty."; + return false; + } + + var newKeyword = NewKeyword.Trim(); + if (oldKeyword == newKeyword) + { + errorMessage = "The new keyword is the same as the old keyword."; + return false; + } + + var alreadySetKeyword = keywords.FirstOrDefault(k => k.Keyword == newKeyword); + if (alreadySetKeyword != null) + { + errorMessage = $"The new keyword matches an already set one:\n" + + $"\"{alreadySetKeyword.Name}\" => \"{alreadySetKeyword.Keyword}\""; + return false; + } + + SelectedKeyword.Keyword = newKeyword; + context.API.SaveSettingJsonStorage(); + return true; + + } + } + +} diff --git a/Flow.Launcher.Plugin.OneNote/UI/ViewModels/KeywordViewModel.cs b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/KeywordViewModel.cs new file mode 100644 index 0000000..b87bd9d --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/KeywordViewModel.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Flow.Launcher.Plugin.OneNote.UI.ViewModels +{ + public partial class KeywordViewModel : BaseModel + { + public object Instance { get; init; } + public PropertyInfo PropertyInfo { get; init; } + public string Name { get; init; } + + public string Keyword + { + get => (string)PropertyInfo.GetValue(Instance); + set + { + PropertyInfo.SetValue(Instance, value, null); + OnPropertyChanged(nameof(Keyword)); + } + } + + + public static KeywordViewModel[] GetKeywordViewModels(Keywords keywords) + { + return keywords.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => new KeywordViewModel + { + Instance = keywords, + PropertyInfo = p, + Name = NicfyPropertyName().Replace(p.Name, " $1"), + }) + .ToArray(); + } + + [GeneratedRegex("(\\B[A-Z])")] + private static partial Regex NicfyPropertyName(); + } +} diff --git a/Flow.Launcher.Plugin.OneNote/UI/ViewModels/SettingsViewModel.cs b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..4a0e9b3 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/ViewModels/SettingsViewModel.cs @@ -0,0 +1,34 @@ + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Flow.Launcher.Plugin.OneNote.UI.ViewModels +{ + public class SettingsViewModel : Model + { + public readonly PluginInitContext context; + public SettingsViewModel(PluginInitContext context, Settings settings) + { + Settings = settings; + this.context = context; + Keywords = KeywordViewModel.GetKeywordViewModels(settings.Keywords); + Icons = Icons.Instance; + } + + public Settings Settings { get; init; } + public KeywordViewModel[] Keywords { get; init; } + public Icons Icons { get; init; } + public KeywordViewModel SelectedKeyword { get; set; } + +#pragma warning disable CA1822 // Mark members as static + public IEnumerable DefaultRecentCountOptions => Enumerable.Range(1, 16); +#pragma warning restore CA1822 // Mark members as static + + public string NotebookIcon => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, Icons.Notebook); + public string SectionIcon => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, Icons.Section); + public void OpenNotebookIconsFolder() => context.API.OpenDirectory(Icons.NotebookIconDirectory); + public void OpenSectionIconsFolder() => context.API.OpenDirectory(Icons.SectionIconDirectory); + public void ClearCachedIcons() => Icons.ClearCachedIcons(); + } +} \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/UI/Views/ChangeKeywordWindow.xaml b/Flow.Launcher.Plugin.OneNote/UI/Views/ChangeKeywordWindow.xaml new file mode 100644 index 0000000..0b3ff0a --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/Views/ChangeKeywordWindow.xaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Flow.Launcher.Plugin.OneNote/UI/Views/ChangeKeywordWindow.xaml.cs b/Flow.Launcher.Plugin.OneNote/UI/Views/ChangeKeywordWindow.xaml.cs new file mode 100644 index 0000000..965dce3 --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/Views/ChangeKeywordWindow.xaml.cs @@ -0,0 +1,38 @@ +using System.Windows; +using Flow.Launcher.Plugin.OneNote.UI.ViewModels; + +namespace Flow.Launcher.Plugin.OneNote.UI.Views +{ + public partial class ChangeKeywordWindow + { + private readonly ChangeKeywordViewModel viewModel; + + public ChangeKeywordWindow(SettingsViewModel settingsViewModel) + { + InitializeComponent(); + DataContext = viewModel = new ChangeKeywordViewModel(settingsViewModel); + } + + private void WindowLoaded(object sender, RoutedEventArgs e) + { + TextBox_NewKeyword.Focus(); + } + + private void CloseWindow(object sender, RoutedEventArgs e) + { + Close(); + } + + private void Button_ChangeKeyword(object sender, RoutedEventArgs e) + { + if(viewModel.ChangeKeyword(out string errorMessage)) + { + Close(); + } + else + { + MessageBox.Show(this, errorMessage,"Invalid Keyword", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + } +} diff --git a/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml b/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml new file mode 100644 index 0000000..fdbb7af --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml.cs b/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml.cs new file mode 100644 index 0000000..a068b9a --- /dev/null +++ b/Flow.Launcher.Plugin.OneNote/UI/Views/SettingsView.xaml.cs @@ -0,0 +1,106 @@ +using Modern = ModernWpf.Controls; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Flow.Launcher.Plugin.OneNote.UI.ViewModels; + +namespace Flow.Launcher.Plugin.OneNote.UI.Views +{ + public partial class SettingsView : UserControl + { + private readonly SettingsViewModel viewModel; + public SettingsView(SettingsViewModel viewModel) + { + InitializeComponent(); + DataContext = this.viewModel = viewModel; + } + + //quick and dirty non MVVM stuffs + private async void ClearCachedIcons(object sender, RoutedEventArgs e) + { + var input = (UIElement)sender; + var temp = input.IsEnabled; + input.IsEnabled = false; + + var dialog = new Modern.ContentDialog() + { + Title = "Clear Cached Icons", + Content = $"Delete cached notebook and sections icons.\n" + + $"This will delete {viewModel.Icons.CachedIconCount} icon{(viewModel.Icons.CachedIconCount != 1 ? "s" : string.Empty)}.", + PrimaryButtonText = "Yes", + CloseButtonText = "Cancel", + DefaultButton = Modern.ContentDialogButton.Close, + }; + + var result = await dialog.ShowAsync(); + + if (result == Modern.ContentDialogResult.Primary) + { + viewModel.ClearCachedIcons(); + input.IsEnabled = false; + } + else + { + input.IsEnabled = temp; + } + } + + private void OpenNotebookIconsFolder(object sender, RoutedEventArgs e) + { + viewModel.OpenNotebookIconsFolder(); + } + + private void OpenSectionIconsFolder(object sender, RoutedEventArgs e) + { + viewModel.OpenSectionIconsFolder(); + } + + private void EditButton_Click(object sender, RoutedEventArgs e) + { + if (viewModel.SelectedKeyword == null) + { + MessageBox.Show("Please select a keyword"); + } + else + { + new ChangeKeywordWindow(viewModel).ShowDialog(); + } + } + + private void ListView_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left) + { + var listView = (ListView)sender; + + var hit = listView.InputHitTest(e.GetPosition(listView)); + if (hit is FrameworkElement fe && fe.DataContext is KeywordViewModel selectedKeyword) + { + listView.SelectedItem = selectedKeyword; + EditButton_Click(sender, e); + } + } + } + + private void ListView_MouseUp(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Right) + { + var listView = (ListView)sender; + + var hit = listView.InputHitTest(e.GetPosition(listView)); + if (hit is FrameworkElement fe && fe.DataContext is KeywordViewModel selectKeyword) + { + listView.SelectedItem = selectKeyword; + + var menuItem = new MenuItem(); + menuItem.Click += EditButton_Click; + menuItem.Header = "Edit"; + var contextMenu = new ContextMenu(); + contextMenu.Items.Add(menuItem); + contextMenu.IsOpen = true; + } + } + } + } +} diff --git a/plugin.json b/Flow.Launcher.Plugin.OneNote/plugin.json similarity index 79% rename from plugin.json rename to Flow.Launcher.Plugin.OneNote/plugin.json index cf28882..a99f8ea 100644 --- a/plugin.json +++ b/Flow.Launcher.Plugin.OneNote/plugin.json @@ -2,9 +2,9 @@ "ID": "506B2EE8-4117-4BDF-8F00-DF58498DF922", "ActionKeyword": "on", "Name": "OneNote", - "Description": "Search OneNote notes", + "Description": "Search and create your OneNote notes", "Author": "Odotocodot", - "Version": "1.1.1", + "Version": "2.0.0", "Language": "csharp", "Website": "https://github.com/Odotocodot/Flow.Launcher.Plugin.OneNote", "IcoPath": "Images/logo.png", diff --git a/Images/logo.png b/Images/logo.png deleted file mode 100644 index 12bd424..0000000 Binary files a/Images/logo.png and /dev/null differ diff --git a/Images/new_notebook.png b/Images/new_notebook.png deleted file mode 100644 index dd39b2d..0000000 Binary files a/Images/new_notebook.png and /dev/null differ diff --git a/Images/new_page.png b/Images/new_page.png deleted file mode 100644 index 250eeec..0000000 Binary files a/Images/new_page.png and /dev/null differ diff --git a/Images/new_section.png b/Images/new_section.png deleted file mode 100644 index ea84278..0000000 Binary files a/Images/new_section.png and /dev/null differ diff --git a/Images/notebook.png b/Images/notebook.png deleted file mode 100644 index 761dc30..0000000 Binary files a/Images/notebook.png and /dev/null differ diff --git a/Images/recent.png b/Images/recent.png deleted file mode 100644 index 0ae02e4..0000000 Binary files a/Images/recent.png and /dev/null differ diff --git a/Images/recent_page.png b/Images/recent_page.png deleted file mode 100644 index cdc920b..0000000 Binary files a/Images/recent_page.png and /dev/null differ diff --git a/Images/refresh.png b/Images/refresh.png deleted file mode 100644 index 9d82734..0000000 Binary files a/Images/refresh.png and /dev/null differ diff --git a/Images/section.png b/Images/section.png deleted file mode 100644 index 14bbee0..0000000 Binary files a/Images/section.png and /dev/null differ diff --git a/Images/unavailable.png b/Images/unavailable.png deleted file mode 100644 index 2608c01..0000000 Binary files a/Images/unavailable.png and /dev/null differ diff --git a/Images/warning.png b/Images/warning.png deleted file mode 100644 index 26871c6..0000000 Binary files a/Images/warning.png and /dev/null differ diff --git a/Main.cs b/Main.cs deleted file mode 100644 index 3f36369..0000000 --- a/Main.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using ScipBe.Common.Office.OneNote; -using System.Linq; - -namespace Flow.Launcher.Plugin.OneNote -{ - public class OneNotePlugin : IPlugin, IContextMenu - { - private PluginInitContext context; - private bool hasOneNote; - private readonly int recentPagesCount = 5; - public IOneNoteExtNotebook lastSelectedNotebook; - public IOneNoteExtSection lastSelectedSection; - - private NotebookExplorer notebookExplorer; - private ResultCreator rc; - - public void Init(PluginInitContext context) - { - this.context = context; - try - { - _ = OneNoteProvider.PageItems.Any(); - hasOneNote = true; - } - catch (Exception) - { - hasOneNote = false; - return; - } - rc = new ResultCreator(context, this); - notebookExplorer = new NotebookExplorer(context, this, rc); - } - - public List Query(Query query) - { - if (!hasOneNote) - { - return new List() - { - new Result - { - Title = "OneNote is not installed.", - IcoPath = Icons.Unavailable - } - }; - } - if (string.IsNullOrEmpty(query.Search)) - { - return new List() - { - new Result - { - Title = "Search OneNote pages", - SubTitle = $"Type \"{Keywords.NotebookExplorer}\" or select this option to search by notebook structure ", - AutoCompleteText = $"{query.ActionKeyword} {Keywords.NotebookExplorer}", - IcoPath = Icons.Logo, - Score = 2000, - Action = c => - { - context.API.ChangeQuery($"{query.ActionKeyword} {Keywords.NotebookExplorer}"); - return false; - }, - }, - new Result - { - Title = "See recent pages", - SubTitle = $"Type \"{Keywords.RecentPages}\" or select this option to see recently modified pages", - AutoCompleteText = $"{query.ActionKeyword} {Keywords.RecentPages}", - IcoPath = Icons.Recent, - Score = -1000, - Action = c => - { - context.API.ChangeQuery($"{query.ActionKeyword} {Keywords.RecentPages}"); - return false; - }, - }, - new Result - { - Title = "New quick note", - IcoPath = Icons.NewPage, - Score = -4000, - Action = c => - { - ScipBeExtensions.CreateAndOpenPage(); - return true; - } - }, - new Result - { - Title = "Open and sync notebooks", - IcoPath = Icons.Sync, - Score = int.MinValue, - Action = c => - { - OneNoteProvider.NotebookItems.OpenAndSync(OneNoteProvider.PageItems.First()); - return false; - } - }, - }; - } - if (query.FirstSearch.StartsWith(Keywords.RecentPages)) - { - int count = recentPagesCount; - if (query.FirstSearch.Length > Keywords.RecentPages.Length && int.TryParse(query.FirstSearch[Keywords.RecentPages.Length..], out int userChosenCount)) - count = userChosenCount; - - return OneNoteProvider.PageItems.OrderByDescending(pg => pg.LastModified) - .Take(count) - .Select(pg => - { - Result result = rc.CreatePageResult(pg); - result.SubTitle = $"{GetLastEdited(DateTime.Now - pg.LastModified)}\t{result.SubTitle}"; - result.IcoPath = Icons.RecentPage; - return result; - }) - .ToList(); - } - - //Search via notebook structure - //NOTE: There is no nested sections i.e. there is nothing for the Section Group in the structure - if (query.FirstSearch.StartsWith(Keywords.NotebookExplorer)) - return notebookExplorer.Explore(query); - - //Check for invalid start of query i.e. symbols - if (!char.IsLetterOrDigit(query.Search[0])) - return new List() - { - new Result - { - Title = "Invalid query", - SubTitle = "The first character of the search must be a letter or a digit", - IcoPath = Icons.Warning, - } - }; - //Default search - var searches = OneNoteProvider.FindPages(query.Search) - .Select(pg => rc.CreatePageResult(pg, context.API.FuzzySearch(query.Search, pg.Name).MatchData)); - - if (searches.Any()) - return searches.ToList(); - - return new List - { - new Result - { - Title = "No matches found", - SubTitle = "Try searching something else, or syncing your notebooks.", - IcoPath = Icons.Logo, - } - }; - } - - public List LoadContextMenus(Result selectedResult) - { - List results = new List(); - switch (selectedResult.ContextData) - { - case IOneNoteExtNotebook notebook: - Result result = rc.CreateNotebookResult(notebook); - result.Title = "Open and sync notebook"; - result.SubTitle = notebook.Name; - result.ContextData = null; - result.Action = c => - { - notebook.OpenAndSync(); - lastSelectedNotebook = null; - return true; - }; - results.Add(result); - break; - case IOneNoteExtSection section: - Result sResult = rc.CreateSectionResult(section, lastSelectedNotebook); - sResult.Title = "Open and sync section"; - sResult.SubTitle = section.Name; - sResult.ContextData = null; - sResult.Action = c => - { - section.OpenAndSync(); - lastSelectedNotebook = null; - lastSelectedSection = null; - return true; - }; - Result nbResult = rc.CreateNotebookResult(lastSelectedNotebook); - nbResult.Title = "Open and sync notebook"; - nbResult.SubTitle = lastSelectedNotebook.Name; - nbResult.Action = c => - { - lastSelectedNotebook.OpenAndSync(); - lastSelectedNotebook = null; - lastSelectedSection = null; - return true; - }; - results.Add(sResult); - results.Add(nbResult); - break; - } - return results; - } - - private static string GetLastEdited(TimeSpan diff) - { - string lastEdited = "Last edited "; - if (PluralCheck(diff.TotalDays, "day", ref lastEdited) - || PluralCheck(diff.TotalHours, "hour", ref lastEdited) - || PluralCheck(diff.TotalMinutes, "min", ref lastEdited) - || PluralCheck(diff.TotalSeconds, "sec", ref lastEdited)) - return lastEdited; - else - return lastEdited += "Now."; - - bool PluralCheck(double totalTime, string timeType, ref string lastEdited) - { - var roundedTime = (int)Math.Round(totalTime); - if (roundedTime > 0) - { - string plural = roundedTime == 1 ? "" : "s"; - lastEdited += $"{roundedTime} {timeType}{plural} ago."; - return true; - } - else - return false; - - } - } - } -} \ No newline at end of file diff --git a/NotebookExplorer.cs b/NotebookExplorer.cs deleted file mode 100644 index 4f9ce31..0000000 --- a/NotebookExplorer.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System; -using System.Collections.Generic; -using ScipBe.Common.Office.OneNote; -using System.Linq; - -namespace Flow.Launcher.Plugin.OneNote -{ - public class NotebookExplorer - { - private PluginInitContext context; - private OneNotePlugin oneNotePlugin; - - private IOneNoteExtNotebook LastSelectedNotebook { get => oneNotePlugin.lastSelectedNotebook; set => oneNotePlugin.lastSelectedNotebook = value; } - private IOneNoteExtSection LastSelectedSection { get => oneNotePlugin.lastSelectedSection; set => oneNotePlugin.lastSelectedSection = value; } - - private ResultCreator rc; - - - public NotebookExplorer(PluginInitContext context, OneNotePlugin oneNotePlugin, ResultCreator resultCreator) - { - this.context = context; - this.oneNotePlugin = oneNotePlugin; - rc = resultCreator; - } - - public List Explore(Query query) - { - string[] searchStrings = query.Search.Split('\\', StringSplitOptions.None); - //Could replace switch case with for loop - switch (searchStrings.Length) - { - case 2://Full query for notebook not complete e.g. nb\User Noteb - //Get matching notebooks and create results. - return GetNotebooks(searchStrings); - - case 3://Full query for section not complete e.g nb\User Notebook\Happine - return GetSections(searchStrings); - - case 4://Searching pages in a section - return GetPages(searchStrings); - - default: - return new List(); - } - } - - private List GetNotebooks(string[] searchStrings) - { - List results = new List(); - string query = searchStrings[1]; - - if (string.IsNullOrWhiteSpace(query)) // Do a normal notebook search - { - LastSelectedNotebook = null; - results = OneNoteProvider.NotebookItems.Select(nb => rc.CreateNotebookResult(nb)).ToList(); - return results; - } - List highlightData = null; - - results = OneNoteProvider.NotebookItems.Where(nb => - { - if (LastSelectedNotebook != null && nb.ID == LastSelectedNotebook.ID) - return true; - - return TreeQuery(nb.Name, query, out highlightData); - }) - .Select(nb => rc.CreateNotebookResult(nb, highlightData)) - .ToList(); - - if (!results.Any(result => string.Equals(query.Trim(), result.Title, StringComparison.OrdinalIgnoreCase))) - { - results.Add(rc.CreateNewNotebookResult(query)); - } - return results; - } - - private List GetSections(string[] searchStrings) - { - List results = new List(); - - string query = searchStrings[2]; - - if (!ValidateNotebook(searchStrings[1])) - return results; - - if (string.IsNullOrWhiteSpace(query)) - { - LastSelectedSection = null; - results = LastSelectedNotebook.Sections.Where(s => !s.Encrypted) - .Select(s => rc.CreateSectionResult(s, LastSelectedNotebook)) - .ToList(); - - //if no sections show ability to create section - if (!results.Any()) - { - results.Add(new Result - { - Title = "Create section: \"\"", - SubTitle = "No (unencrypted) sections found. Type a valid title to create one", - IcoPath = Icons.NewSection - }); - } - return results; - } - - List highlightData = null; - - results = LastSelectedNotebook.Sections.Where(s => - { - if (s.Encrypted) - return false; - - if (LastSelectedSection != null && s.ID == LastSelectedSection.ID) - return true; - - return TreeQuery(s.Name, query, out highlightData); - }) - .Select(s => rc.CreateSectionResult(s, LastSelectedNotebook, highlightData)) - .ToList(); - if (!results.Any(result => string.Equals(query.Trim(), result.Title, StringComparison.OrdinalIgnoreCase))) - { - results.Add(rc.CreateNewSectionResult(LastSelectedNotebook, query)); - } - return results; - } - - private List GetPages(string[] searchStrings) - { - List results = new List(); - - string query = searchStrings[3]; - - if (!ValidateNotebook(searchStrings[1])) - return results; - - if (!ValidateSection(searchStrings[2])) - return results; - - if (string.IsNullOrWhiteSpace(query)) - { - results = LastSelectedSection.Pages.Select(pg => rc.CreatePageResult(pg, LastSelectedSection, LastSelectedNotebook)).ToList(); - //if no sections show ability to create section - if (!results.Any()) - { - results.Add(new Result - { - Title = "Create page: \"\"", - SubTitle = "No pages found. Type a valid title to create one", - IcoPath = Icons.NewPage - }); - } - return results; - } - - List highlightData = null; - - results = LastSelectedSection.Pages.Where(pg => TreeQuery(pg.Name, query, out highlightData)) - .Select(pg => rc.CreatePageResult(pg, LastSelectedSection, LastSelectedNotebook, highlightData)) - .ToList(); - if (!results.Any(result => string.Equals(query.Trim(), result.Title, StringComparison.OrdinalIgnoreCase))) - { - results.Add(rc.CreateNewPageResult(LastSelectedSection, LastSelectedNotebook, query)); - } - return results; - } - - - private bool ValidateNotebook(string notebookName) - { - if (LastSelectedNotebook == null) - { - var notebook = OneNoteProvider.NotebookItems.FirstOrDefault(nb => nb.Name == notebookName); - if (notebook == null) - return false; - LastSelectedNotebook = notebook; - return true; - } - return true; - } - - private bool ValidateSection(string sectionName) - { - if (LastSelectedSection == null) //Check if section is valid - { - var section = LastSelectedNotebook.Sections.FirstOrDefault(s => s.Name == sectionName); - if (section == null || section.Encrypted) - return false; - LastSelectedSection = section; - return true; - } - return true; - } - private bool TreeQuery(string itemName, string searchString, out List highlightData) - { - var matchResult = context.API.FuzzySearch(searchString, itemName); - highlightData = matchResult.MatchData; - return matchResult.IsSearchPrecisionScoreMet(); - } - } -} \ No newline at end of file diff --git a/Odotocodot.OneNote.Linq b/Odotocodot.OneNote.Linq new file mode 160000 index 0000000..f1cf1d0 --- /dev/null +++ b/Odotocodot.OneNote.Linq @@ -0,0 +1 @@ +Subproject commit f1cf1d0204b43a7d982a9ea580a3945d23c6be12 diff --git a/OneNoteItemInfo.cs b/OneNoteItemInfo.cs deleted file mode 100644 index 8059ecb..0000000 --- a/OneNoteItemInfo.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Runtime.InteropServices; - -namespace Flow.Launcher.Plugin.OneNote -{ - public class OneNoteItemInfo - { - private Dictionary icons; - private string iconDirectory; - private string baseIconPath; - - public OneNoteItemInfo(string folderName, string iconName, PluginInitContext context) - { - icons = new Dictionary(); - iconDirectory = Path.Combine(context.CurrentPluginMetadata.PluginDirectory, folderName); - baseIconPath = Path.Combine(context.CurrentPluginMetadata.PluginDirectory, iconName); - Directory.CreateDirectory(iconDirectory); - foreach (var imagePath in Directory.GetFiles(iconDirectory)) - { - if (int.TryParse(Path.GetFileNameWithoutExtension(imagePath), out int argb)) - icons.Add(Color.FromArgb(argb), imagePath); - } - } - public string GetIcon(Color color) - { - if (!icons.TryGetValue(color, out string path)) - { - //Create Colored Image - using (var bitmap = new Bitmap(baseIconPath)) - { - BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, bitmap.PixelFormat); - - int bytesPerPixel = Bitmap.GetPixelFormatSize(bitmap.PixelFormat) / 8; - byte[] pixels = new byte[bitmapData.Stride * bitmap.Height]; - IntPtr pointer = bitmapData.Scan0; - Marshal.Copy(pointer, pixels, 0, pixels.Length); - int bytesWidth = bitmapData.Width * bytesPerPixel; - - for (int j = 0; j < bitmapData.Height; j++) - { - int line = j * bitmapData.Stride; - for (int i = 0; i < bytesWidth; i = i + bytesPerPixel) - { - pixels[line + i] = color.B; - pixels[line + i + 1] = color.G; - pixels[line + i + 2] = color.R; - } - } - - Marshal.Copy(pixels, 0, pointer, pixels.Length); - bitmap.UnlockBits(bitmapData); - path = Path.Combine(iconDirectory, color.ToArgb() + ".png"); - bitmap.Save(path, ImageFormat.Png); - } - icons.Add(color, path); - } - return path; - } - } -} \ No newline at end of file diff --git a/Readme.md b/Readme.md index 5c13f5c..ccb3305 100644 --- a/Readme.md +++ b/Readme.md @@ -1,39 +1,223 @@ -# OneNote for Flow Launcher +

+ + + + + + +

+

OneNote for Flow Launcher

-A OneNote plugin for the [Flow launcher](https://github.com/Flow-Launcher/Flow.Launcher), allowing for the ability to quickly access and create notes. -![demo](doc/demo.png) +A [OneNote](https://www.microsoft.com/en-gb/microsoft-365/onenote/digital-note-taking-app) plugin for the [Flow launcher](https://github.com/Flow-Launcher/Flow.Launcher), allowing for the ability to quickly access and create notes. -## Features and Usage + +## Contents -| Keyword | Name | Description | -|---------------------------------|------|---------------| -| `` on {OneNote search query} `` | [Default Search](#default-search) | Search OneNote pages, searches page title and content | -| `` on nb:\ `` | [Notebook Explorer](#notebook-explorer) | Navigate notebooks, sections and pages explorer style | -| `` on rcntpgs: `` | [Recent Pages](#recent-pages) | View recently modified pages | +- [Installation](#installation) +- [Features](#features) + - [At a Glance](#at-a-glance) + - [Default Search](#default-search) + - [Notebook Explorer](#notebook-explorer) + - [Create New Items](#create-new-items) + - [Recent Pages](#recent-pages) + - [Scoped Search](#scoped-search) + - [Title Search](#title-search) +- [Settings](#settings) + - [Keywords](#keywords) +- [Changelog](#200---2023-10-05) +- [Acknowledgements](#acknowledgements) + +## Installation + +Using Flow Launcher type: + +``` +pm install OneNote +``` + +> [!IMPORTANT] +> For [version 2.0+](#200---2023-10-05) requires at Flow Launcher version 1.16+. For earlier versions see [releases](https://github.com/Odotocodot/Flow.Launcher.Plugin.OneNote/releases). + +## Features + +### At a Glance + +| Keyword | Name | Description | +| -------------------------- | --------------------------------------- | ---------------------------- | +| ` on {your search query} ` | [Default Search](#default-search) | Search OneNote pages | +| ` on nb:\ ` | [Notebook Explorer](#notebook-explorer) | Navigate notebooks hierarchy | +| ` on rcntpgs: ` | [Recent Pages](#recent-pages) | View recently modified pages | + + +#### Modifiers + +| Keyword | Name | Notes | +| ------- | ------------------------------- | -------------------------------------------- | +| ` > ` | [Scoped Search](#scoped-search) | Search only within a specific hierarchy item | +| ` * ` | [Title Search](#title-search) | Search only the title of hierarchy items | + +> [!NOTE] +> Hierarchy items are notebooks, section groups, sections and pages. ### Default Search -![searchgif](doc/search_pages.gif) +``` +on {your search query} +``` + +This is allows you to search OneNote pages using the OneNote API which searches both the content in a page as well as the page title. + +- Press ⏎ Enter or left-click on a search result to open in OneNote. + +> [!NOTE] +> You can include bitwise operators like `AND` or `OR` (they must be uppercase) in your search. E.g. `on hello there AND general kenobi`. + +![default search gif](doc/default_search.gif) ### Notebook Explorer -- Allows for creating notebooks at the default notebook location (set in OneNote) as well as sections and pages at the current path. -- Pressing ⏎ Enter or ⇥ Tab on a notebook or section will auto complete the query. -- Pressing ⇧ Shift + ⏎ Enter on a notebook or section allows you to open it directly in OneNote. -- Encrypted sections are hidden. +``` +on nb:\ +``` + +Traverse your OneNote notebooks explorer style. + +- Press ⏎ Enter or ⇥ Tab or left-click on a result to auto complete the query. +- Press ⇧ Shift + ⏎ Enter or right-click on a result to open it directly in OneNote. + +![notebook explorer gif](doc/notebook_explorer.gif) + +> [!NOTE] +> Supports all OneNote hierarchy items i.e. notebooks, section groups, sections and pages. + +#### Create New Items + +Whilst using the notebook explorer, if your search query does not match any names of the items in the results, the plugin will give you an option to create a new item. -![notebookgif](doc/notebook_explorer.gif) +![create new item gif](doc/create_new_items.gif) +> [!NOTE] +> Supports all OneNote hierarchy items i.e. notebooks, section groups, sections and pages. +> ### Recent Pages -Add a number after `` rcntpgs: `` to display that number of recent pages. E.g. `` rcntpgs:10 `` will show the 10 most recently modified pages. +``` +on rcntpgs: +``` -![recentgif](doc/recent_pages.gif) +Displays your recently modified OneNote pages. -## Acknowledgements +Add a number after `` rcntpgs: `` to display that number of recent pages. E.g. the full query ``on rcntpgs:10`` will show the 10 most recently modified pages. + +![recent pages gif](doc/recent_pages.gif) + +### Scoped Search + +``` +on nb:\{hierarchy path}\>{your search query} +``` + +Scoped search is the same as [default search](#default-search) but restricted to search within a specific hierarchy item. + +> [!IMPORTANT] +> Must be used with [notebook explorer](#notebook-explorer). -Inspired by the OneNote plugin for [PowerToys](https://github.com/microsoft/PowerToys/tree/main/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote) (Its a port with extra features) +![scoped search gif](doc/scoped_search.gif) -Icons from [Icons8](https://icons8.com) +
+

Example Queries

+ + ``` + on nb:\A Notebook\A Section Group\>hello there + ``` + + Will use the OneNote API to search the hierarchy item `A Section Group` for `hello there` + +
+ +### Title Search + +``` +on *{your search query} +``` + +Searches for hierarchy items based on their title alone. Unlike [scoped search](#scoped-search) and [default search](#default-search) which only return pages, title search can return all hierarchy item types. + +> [!NOTE] +> Can be used in conjunction with [notebook explorer](#notebook-explorer). + +![title search gif](doc/title_search_default.gif) +![title search gif](doc/title_search_notebook.gif) + +
+

Example Queries

+ + ``` + on nb:\A Notebook\A Section Group\*hello there + ``` + + Will search for titles in the hierarchy item `A Section Group` that match or are similar to `hello there` + +
+ +## Settings + +The plugin has a number of settings such as the ability to hide the recycle bin or indicate which pages have unread changes. + +
+ Screenshot of settings + settings +
+ +### Keywords + +All the keywords used can be changed according to user preference. + +
+ Screenshot of keywords + keywords +
+ +
+

2.0.0 Changelog: Massive Refactor and New Features

+ + +### 2.0.0 - 2023-10-05 + + +#### Added + +- **[Created custom OneNote parser/library](https://github.com/Odotocodot/Linq2OneNote)**, adding the ability for several new features. +- Support for section groups when using the notebook explorer. +- Support for displaying unread results. +- Support for showing locked sections in results (you still can't see inside them unless they are unlocked). +- The ability to search by only title. +- The ability to do a scoped search (e.g. search in one section only). +- **Settings!** You can change these options: + - Show unread icons. + - Show encrypted sections. + - Show recycle bin items. + - Created coloured icons for notebook and sections. + - Default number of recent pages + - **Customisable keywords!** + + +#### Changed + +- Compressed images. +- Reduced the calls to create a OneNote COM object, this should lead to a overall smoother experience. +- Updated to .NET 7 (update Flow Launcher if an error persists). +- Refactored majority of code and project structure. + + +#### Removed + +- [Scipbe.Common.Office.OneNote](https://github.com/scipbe/ScipBe-Common-Office) package reference. + +
+ +## Acknowledgements -Created with [ScipBe](https://github.com/scipbe/ScipBe-Common-Office) OneNote tools +- Made with [Linq2OneNote](https://github.com/Odotocodot/Linq2OneNote) a library for exposing the OneNote API also made by me :smiley: +- Inspired by the OneNote plugin for [PowerToys](https://github.com/microsoft/PowerToys/tree/main/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote) +- Icons from [Icons8](https://icons8.com) diff --git a/ResultCreator.cs b/ResultCreator.cs deleted file mode 100644 index 8f0b3d6..0000000 --- a/ResultCreator.cs +++ /dev/null @@ -1,168 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Linq; -using ScipBe.Common.Office.OneNote; - -namespace Flow.Launcher.Plugin.OneNote -{ - public class ResultCreator - { - private PluginInitContext context; - private OneNotePlugin oneNotePlugin; - private OneNoteItemInfo notebookInfo; - private OneNoteItemInfo sectionInfo; - - private IOneNoteExtNotebook LastSelectedNotebook { get => oneNotePlugin.lastSelectedNotebook; set => oneNotePlugin.lastSelectedNotebook = value; } - private IOneNoteExtSection LastSelectedSection { get => oneNotePlugin.lastSelectedSection; set => oneNotePlugin.lastSelectedSection = value; } - - - public ResultCreator(PluginInitContext context, OneNotePlugin oneNotePlugin) - { - this.context = context; - this.oneNotePlugin = oneNotePlugin; - notebookInfo = new OneNoteItemInfo("Images/NotebookIcons", Icons.Notebook, context); - sectionInfo = new OneNoteItemInfo("Images/SectionIcons", Icons.Section, context); - } - - private string GetNicePath(IOneNoteSection section, IOneNoteNotebook notebook, bool isPage) - { - int offset = isPage - ? 4 //"4" is to remove the ".one" from the path - : section.Name.Length + 5; //The "+5" is to remove the ".one" and "/" from the path - var sectionPath = section.Path; - var index = sectionPath.IndexOf(notebook.Name); - var path = sectionPath[index..^offset] - .Replace("/", " > ") - .Replace("\\", " > "); - return path; - } - - - public Result CreatePageResult(IOneNoteExtPage page, List highlightingData = null) - { - return CreatePageResult(page, page.Section, page.Notebook, highlightingData); - } - - public Result CreatePageResult(IOneNotePage page, IOneNoteSection section, IOneNoteNotebook notebook, List highlightingData = null) - { - return new Result - { - Title = page.Name, - TitleToolTip = $"Created: {page.DateTime}\nLast Modified: {page.LastModified}", - TitleHighlightData = highlightingData, - SubTitle = GetNicePath(section, notebook, true), - IcoPath = Icons.Logo, - ContextData = page, - Action = c => - { - LastSelectedNotebook = null; - LastSelectedSection = null; - page.OpenInOneNote(); - return true; - }, - }; - } - - public Result CreateSectionResult(IOneNoteExtSection section, IOneNoteExtNotebook notebook, List highlightData = null) - { - string path = GetNicePath(section, notebook, false); - string autoCompleteText = $"{context.CurrentPluginMetadata.ActionKeyword} {Keywords.NotebookExplorer}{notebook.Name}\\{section.Name}\\"; - return new Result - { - Title = section.Name, - TitleHighlightData = highlightData, - SubTitle = path, - SubTitleToolTip = $"{path} | Number of pages: {section.Pages.Count()}", - AutoCompleteText = autoCompleteText, - ContextData = section, - IcoPath = sectionInfo.GetIcon(section.Color.Value), - Action = c => - { - LastSelectedSection = section; - context.API.ChangeQuery(autoCompleteText); - return false; - }, - }; - } - - public Result CreateNotebookResult(IOneNoteExtNotebook notebook, List highlightData = null) - { - string autoCompleteText = $"{context.CurrentPluginMetadata.ActionKeyword} {Keywords.NotebookExplorer}{notebook.Name}\\"; - return new Result - { - Title = notebook.Name, - TitleToolTip = $"Number of sections: {notebook.Sections.Count()}", - TitleHighlightData = highlightData, - AutoCompleteText = autoCompleteText, - ContextData = notebook, - IcoPath = notebookInfo.GetIcon(notebook.Color.Value), - Action = c => - { - LastSelectedNotebook = notebook; - context.API.ChangeQuery(autoCompleteText); - return false; - }, - }; - } - - - public Result CreateNewPageResult(IOneNoteSection section, IOneNoteNotebook notebook, string pageTitle) - { - pageTitle = pageTitle.Trim(); - return new Result - { - Title = $"Create page: \"{pageTitle}\"", - SubTitle = $"Path: {GetNicePath(section,notebook,true)}", - IcoPath = Icons.NewPage, - Action = c => - { - ScipBeExtensions.CreateAndOpenPage(LastSelectedSection, pageTitle); - LastSelectedNotebook = null; - LastSelectedSection = null; - return true; - } - }; - } - - public Result CreateNewSectionResult(IOneNoteNotebook notebook, string sectionTitle) - { - sectionTitle = sectionTitle.Trim(); - return new Result - { - Title = $"Create section: \"{sectionTitle}\"", - SubTitle = $"Path: {notebook.Name}", - IcoPath = Icons.NewSection, - Action = c => - { - ScipBeExtensions.CreateAndOpenSection(LastSelectedNotebook,sectionTitle); - context.API.ChangeQuery(context.CurrentPluginMetadata.ActionKeyword); - LastSelectedNotebook = null; - LastSelectedSection = null; - return true; - } - }; - } - - public Result CreateNewNotebookResult(string notebookTitle) - { - notebookTitle = notebookTitle.Trim(); - return new Result - { - Title = $"Create notebook: \"{notebookTitle}\"", - //TitleHighlightData = context.API.FuzzySearch(notebookTitle,title).MatchData, - SubTitle = $"Location: {ScipBeExtensions.GetDefaultNotebookLocation()}", - IcoPath = Icons.NewNotebook, - Action = c => - { - ScipBeExtensions.CreateAndOpenNotebook(context,notebookTitle); - context.API.ChangeQuery(context.CurrentPluginMetadata.ActionKeyword); - LastSelectedNotebook = null; - LastSelectedSection = null; - return true; - } - }; - } - - } -} \ No newline at end of file diff --git a/ScipBeExtensions.cs b/ScipBeExtensions.cs deleted file mode 100644 index ab499d4..0000000 --- a/ScipBeExtensions.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; -using Microsoft.Office.Interop.OneNote; -using ScipBe.Common.Office.OneNote; -using static Flow.Launcher.Plugin.OneNote.ScipBeUtils.Utils; - -namespace Flow.Launcher.Plugin.OneNote -{ - public static class ScipBeExtensions - { - public static void OpenAndSync(this IOneNoteSection section) - { - CallOneNoteSafely(oneNote => - { - oneNote.NavigateTo(section.ID); - oneNote.SyncHierarchy(section.ID); - }); - } - - public static void OpenAndSync(this IOneNoteNotebook notebook) - { - CallOneNoteSafely(oneNote => - { - oneNote.NavigateTo(notebook.ID); - oneNote.SyncHierarchy(notebook.ID); - }); - } - public static void OpenAndSync(this IEnumerable notebooks, IOneNotePage lastModifiedPage) - { - CallOneNoteSafely(oneNote => - { - foreach (var notebook in notebooks) - { - oneNote.SyncHierarchy(notebook.ID); - } - oneNote.NavigateTo(lastModifiedPage.ID); - }); - } - - public static string GetDefaultNotebookLocation() - { - return CallOneNoteSafely(oneNote => - { - oneNote.GetSpecialLocation(SpecialLocation.slDefaultNotebookFolder, out string path); - return path; - }); - } - public static void CreateAndOpenPage(IOneNoteSection section, string pageTitle) - { - CallOneNoteSafely(oneNote => - { - oneNote.GetHierarchy(null, HierarchyScope.hsNotebooks, out string xmlNb); - - XNamespace ns = XDocument.Parse(xmlNb).Root.Name.Namespace; - - oneNote.CreateNewPage(section.ID, out string pageID, NewPageStyle.npsBlankPageWithTitle); - - oneNote.GetPageContent(pageID, out string xml, PageInfo.piBasic); - XDocument doc = XDocument.Parse(xml); - XElement Xtitle = doc.Descendants(ns + "T").First(); - Xtitle.Value = pageTitle; - - oneNote.UpdatePageContent(doc.ToString()); - - oneNote.SyncHierarchy(pageID); - oneNote.NavigateTo(pageID); - }); - } - - public static void CreateAndOpenPage() - { - CallOneNoteSafely(oneNote => - { - oneNote.GetSpecialLocation(SpecialLocation.slUnfiledNotesSection, out string path); - oneNote.OpenHierarchy(path, null, out string sectionID, CreateFileType.cftNone); - - oneNote.CreateNewPage(sectionID, out string pageID, NewPageStyle.npsDefault); - - oneNote.SyncHierarchy(pageID); - oneNote.NavigateTo(pageID); - }); - } - - public static void CreateAndOpenSection(this IOneNoteNotebook notebook, string title) - { - CallOneNoteSafely(oneNote => - { - oneNote.OpenHierarchy(title + ".one", notebook.ID, out string sectionID, CreateFileType.cftSection); - - oneNote.SyncHierarchy(sectionID); - oneNote.NavigateTo(sectionID); - }); - } - - public static void CreateAndOpenNotebook(PluginInitContext context,string title) - { - CallOneNoteSafely(oneNote => - { - oneNote.GetSpecialLocation(SpecialLocation.slDefaultNotebookFolder, out string path); - - oneNote.OpenHierarchy($"{path}\\{title}", null, out string notebookID, CreateFileType.cftNotebook); - - oneNote.SyncHierarchy(notebookID); - oneNote.NavigateTo(notebookID); - }); - } - - } -} diff --git a/ScipBeUtils.cs b/ScipBeUtils.cs deleted file mode 100644 index 55cc40b..0000000 --- a/ScipBeUtils.cs +++ /dev/null @@ -1,110 +0,0 @@ -// MIT License - -// Copyright (c) 2017 Stefan Cruysberghs - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -using System; -using System.Runtime.InteropServices; -using Microsoft.Office.Interop.OneNote; - -namespace Flow.Launcher.Plugin.OneNote.ScipBeUtils -{ - /// - /// Utils from ScipBe.Common.Office.OneNote - /// - public static class Utils - { - /// - /// Do retry and return some value - /// - /// Return type - /// Type of exception to catch - /// Action to perform - /// Interval between retries - /// How many times to retry - /// Action to call on retry - /// Action return - public static TRet TryCatchAndRetry(Func action, TimeSpan retryInterval, int retryCount, Action onRetry = null) - where TException : Exception - { - int attempt = 0; - while (true) - { - try - { - return action(); - } - catch (TException ex) - { - if (attempt++ < retryCount) - { - onRetry(ex); - System.Threading.Thread.Sleep(retryInterval); - } - else - { - throw; - } - } - } - } - - internal static void CallOneNoteSafely(Action action) - { - Application oneNote = null; - try - { - oneNote = TryCatchAndRetry( - () => new Application(), - TimeSpan.FromMilliseconds(100), - 3, - ex => System.Diagnostics.Trace.TraceError(ex.Message)); - action(oneNote); - } - finally - { - if (oneNote != null) - { - Marshal.ReleaseComObject(oneNote); - } - } - } - internal static T CallOneNoteSafely(Func action) - { - Application oneNote = null; - try - { - oneNote = TryCatchAndRetry( - () => new Application(), - TimeSpan.FromMilliseconds(100), - 3, - ex => System.Diagnostics.Trace.TraceError(ex.Message)); - return action(oneNote); - } - finally - { - if (oneNote != null) - { - Marshal.ReleaseComObject(oneNote); - } - } - } - } -} diff --git a/doc/create_new_items.gif b/doc/create_new_items.gif new file mode 100644 index 0000000..e94cf82 Binary files /dev/null and b/doc/create_new_items.gif differ diff --git a/doc/default.png b/doc/default.png deleted file mode 100644 index fce01c6..0000000 Binary files a/doc/default.png and /dev/null differ diff --git a/doc/default_search.gif b/doc/default_search.gif new file mode 100644 index 0000000..7bdebc3 Binary files /dev/null and b/doc/default_search.gif differ diff --git a/doc/demo.gif b/doc/demo.gif deleted file mode 100644 index 5332dc8..0000000 Binary files a/doc/demo.gif and /dev/null differ diff --git a/doc/demo.png b/doc/demo.png deleted file mode 100644 index 26a129d..0000000 Binary files a/doc/demo.png and /dev/null differ diff --git a/doc/flow.png b/doc/flow.png new file mode 100644 index 0000000..bc5701a Binary files /dev/null and b/doc/flow.png differ diff --git a/doc/keywords.png b/doc/keywords.png new file mode 100644 index 0000000..1231593 Binary files /dev/null and b/doc/keywords.png differ diff --git a/doc/notebook_explorer.gif b/doc/notebook_explorer.gif index c90a6ad..ed0d6a9 100644 Binary files a/doc/notebook_explorer.gif and b/doc/notebook_explorer.gif differ diff --git a/doc/onenote.png b/doc/onenote.png new file mode 100644 index 0000000..2fec1ba Binary files /dev/null and b/doc/onenote.png differ diff --git a/doc/recent_pages.gif b/doc/recent_pages.gif index 74b9b44..df5ace8 100644 Binary files a/doc/recent_pages.gif and b/doc/recent_pages.gif differ diff --git a/doc/scoped_search.gif b/doc/scoped_search.gif new file mode 100644 index 0000000..fa0f3fa Binary files /dev/null and b/doc/scoped_search.gif differ diff --git a/doc/search_pages.gif b/doc/search_pages.gif deleted file mode 100644 index e664d9a..0000000 Binary files a/doc/search_pages.gif and /dev/null differ diff --git a/doc/settings.png b/doc/settings.png new file mode 100644 index 0000000..bcc8d43 Binary files /dev/null and b/doc/settings.png differ diff --git a/doc/title_search_default.gif b/doc/title_search_default.gif new file mode 100644 index 0000000..2235b51 Binary files /dev/null and b/doc/title_search_default.gif differ diff --git a/doc/title_search_notebook.gif b/doc/title_search_notebook.gif new file mode 100644 index 0000000..409a5cb Binary files /dev/null and b/doc/title_search_notebook.gif differ diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000..eaf3f57 --- /dev/null +++ b/release.ps1 @@ -0,0 +1,66 @@ +$FolderName = 'Flow.Launcher.Plugin.OneNote' +$PluginJson = Get-Content .\$FolderName\plugin.json -Raw | ConvertFrom-Json + +$Name = $PluginJson.Name +$Version = $PluginJson.Version +$ActionKeyword = $PluginJson.ActionKeyword + +if (!$Name) { + Write-Host 'Invalid Name' + Exit +} + +$Choices = @('&Yes', '&No') + +$Choice1 = $Host.UI.PromptForChoice('Name Check', "Is the plugin name valid: $($Name)?", $Choices, 0) +if ($Choice1 -eq 1) { + Write-Host "Invalid Name Cancelling Release" + Exit +} + +$Choice2 = $Host.UI.PromptForChoice('Create Zip', 'Do you want to create a zip file?', $Choices, 1) + +$Choice3 = $Host.UI.PromptForChoice('Auto Show', 'Do you want to automatically show the plugin?', $Choices, 0) + +$FullName = $Name + '-' + $Version + +dotnet publish -c Release -r win-x64 --property:PublishDir=.\bin\Release\$FullName --no-self-contained + +if ($Choice2 -eq 0) { + Write-Host "Creating Zip at: .\$FolderName\bin\$FullName.zip" + Compress-Archive -LiteralPath .\$FolderName\bin\Release\$FullName -DestinationPath .\$FolderName\bin\"$FullName.zip" -Force +} + +Do { + $Flow = Get-Process | Where-Object -Property ProcessName -eq 'Flow.Launcher' + if ($Flow) { + Stop-Process $Flow + Start-Sleep 1 + } +} Until (!$Flow) + +$Folders = Get-ChildItem -Path $env:APPDATA\FlowLauncher\Plugins\ | Where-Object { $_ -Match "$Name-\d.\d.\d" } +foreach ($Folder in $Folders) { + Remove-Item -Recurse $env:APPDATA\FlowLauncher\Plugins\$Folder\ -Force -ErrorAction Stop +} + +Copy-Item -Recurse -LiteralPath ./$FolderName/bin/Release/$FullName $env:APPDATA\FlowLauncher\Plugins\ -Force +$Flow = Start-Process $env:LOCALAPPDATA\FlowLauncher\Flow.Launcher.exe -PassThru + +#Do {} While ($Flow.WaitForInputIdle(5000) -ne $true) +$null = $Flow.WaitForInputIdle(5000) + +# while ($Flow.MainWindowTitle -eq 0) +# { +# Start-Sleep -Milliseconds 1000 +# } + +if ($Choice3 -eq 0) { + $wshell = New-Object -ComObject wscript.shell; + $wshell.AppActivate('Flow.Launcher') + Start-Sleep 3 + + Add-Type -AssemblyName System.Windows.Forms + [System.Windows.Forms.SendKeys]::SendWait("% ") + [System.Windows.Forms.SendKeys]::SendWait($ActionKeyword) +}