From ff4ba8151bd6b48295edabfe22acaf2fdb659852 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Sun, 25 Feb 2024 20:24:50 -0600 Subject: [PATCH 1/6] feat(clips): use file system to store clips in files. --- MainWindow.axaml | 5 +- Models/ClipDbContext.cs | 36 --------------- Models/ClipRepository.cs | 77 +++++++++++++++++++++++++++++++ SharpFM.csproj | 2 - ViewModels/MainWindowViewModel.cs | 26 ++--------- 5 files changed, 84 insertions(+), 62 deletions(-) delete mode 100644 Models/ClipDbContext.cs create mode 100644 Models/ClipRepository.cs diff --git a/MainWindow.axaml b/MainWindow.axaml index 838df7f..22740c2 100644 --- a/MainWindow.axaml +++ b/MainWindow.axaml @@ -21,6 +21,8 @@ + + @@ -32,9 +34,6 @@ - - - diff --git a/Models/ClipDbContext.cs b/Models/ClipDbContext.cs deleted file mode 100644 index f31ddc2..0000000 --- a/Models/ClipDbContext.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; - - -namespace SharpFM.Models; - -/// -/// Clip Db Context -/// -public class ClipDbContext : DbContext -{ - /// - /// Clips stored in the Db. - /// - public DbSet Clips => Set(); - - /// - /// Database path. - /// - public string DbPath { get; } - - /// - /// Constructor. - /// - public ClipDbContext() - { - var folder = Environment.SpecialFolder.LocalApplicationData; - var path = Environment.GetFolderPath(folder); - DbPath = System.IO.Path.Join(path, "sharpFM.db"); - } - - // The following configures EF to create a Sqlite database file in the - // special "local" folder for your platform. - protected override void OnConfiguring(DbContextOptionsBuilder options) - => options.UseSqlite($"Data Source={DbPath}"); -} diff --git a/Models/ClipRepository.cs b/Models/ClipRepository.cs new file mode 100644 index 0000000..e26a436 --- /dev/null +++ b/Models/ClipRepository.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace SharpFM.Models; + +/// +/// Clip File Repository. +/// +public class ClipRepository +{ + /// + /// Clips stored in the specified folder. + /// + public ICollection Clips { get; init; } + + /// + /// Database path. + /// + public string ClipPath { get; } + + /// + /// Constructor. + /// + public ClipRepository(string? path = null) + { + // default to the local app data folder + \SharpFM, otherwise use provided path + path ??= Path.Join( + path1: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + path2: "SharpFM" + ); + + // ensure the directory exists + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + ClipPath = path; + + // init clips to empty + Clips = []; + } + + /// + /// Load clips from the path specified by . + /// + public void LoadClips() + { + foreach (var clipFile in Directory.EnumerateFiles(ClipPath)) + { + var fi = new FileInfo(clipFile); + + var clip = new Clip + { + ClipName = fi.Name.Replace(fi.Extension, string.Empty), + ClipType = fi.Extension.Replace(".", string.Empty), + ClipXml = File.ReadAllText(clipFile) + }; + + Clips.Add(clip); + } + } + + /// + /// Write all clips to their associated clip type files in the path specified by . + /// + public void SaveChanges() + { + foreach (var clip in Clips) + { + var clipPath = Path.Combine(ClipPath, $"{clip.ClipName}.{clip.ClipType}"); + + File.WriteAllText(clipPath, clip.ClipXml); + } + } +} diff --git a/SharpFM.csproj b/SharpFM.csproj index 71ec593..2931e11 100644 --- a/SharpFM.csproj +++ b/SharpFM.csproj @@ -37,8 +37,6 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 74bc674..510891f 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -27,12 +27,10 @@ public MainWindowViewModel(ILogger logger) { _logger = logger; - using var clipContext = new ClipDbContext(); - clipContext.Database.EnsureCreated(); + var clipContext = new ClipRepository(); + clipContext.LoadClips(); - _logger.LogInformation($"Database path: {clipContext.DbPath}."); - - FileMakerClips = new ObservableCollection(); + FileMakerClips = []; foreach (var clip in clipContext.Clips) { @@ -50,7 +48,7 @@ public MainWindowViewModel(ILogger logger) public void SaveToDb() { - using var clipContext = new ClipDbContext(); + var clipContext = new ClipRepository(); var dbClips = clipContext.Clips.ToList(); @@ -77,20 +75,6 @@ public void SaveToDb() clipContext.SaveChanges(); } - public void ClearDb() - { - using var clipContext = new ClipDbContext(); - - var clips = clipContext.Clips.ToList(); - - foreach (var clip in clips) - { - clipContext.Clips.Remove(clip); - } - - clipContext.SaveChanges(); - } - public void ExitApplication() { if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp) @@ -212,7 +196,7 @@ public async Task CopySelectedToClip() /// /// SharpFM Version. /// - public string Version + public static string Version { get { From d40a0a3300e24a7848ba9f753f7683228cbbe8c8 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Tue, 27 Feb 2024 12:45:57 -0600 Subject: [PATCH 2/6] feat: add picker to load different folders of clips --- App.axaml.cs | 19 +++++++++++++++++++ MainWindow.axaml | 5 ++--- Services/FolderService.cs | 29 +++++++++++++++++++++++++++++ ViewModels/MainWindowViewModel.cs | 29 +++++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 Services/FolderService.cs diff --git a/App.axaml.cs b/App.axaml.cs index 0957eca..9ad5eda 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -4,6 +4,9 @@ using SharpFM.ViewModels; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using System; +using SharpFM.Services; namespace SharpFM; @@ -24,8 +27,24 @@ public override void OnFrameworkInitializationCompleted() { DataContext = new MainWindowViewModel(logger) }; + + var services = new ServiceCollection(); + + services.AddSingleton(x => new FolderService(desktop.MainWindow)); + + Services = services.BuildServiceProvider(); } base.OnFrameworkInitializationCompleted(); } + + /// + /// Get a reference to the current app. + /// + public new static App? Current => Application.Current as App; + + /// + /// Gets the instance to resolve application services. + /// + public IServiceProvider? Services { get; private set; } } \ No newline at end of file diff --git a/MainWindow.axaml b/MainWindow.axaml index 22740c2..5b69ed6 100644 --- a/MainWindow.axaml +++ b/MainWindow.axaml @@ -21,9 +21,9 @@ - + - + @@ -36,7 +36,6 @@ - diff --git a/Services/FolderService.cs b/Services/FolderService.cs new file mode 100644 index 0000000..79a82f7 --- /dev/null +++ b/Services/FolderService.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Platform.Storage; + +namespace SharpFM.Services; + +public class FolderService(Window target) +{ + private readonly Window _target = target; + + public async Task GetFolderAsync() + { + // Get top level from the current control. Alternatively, you can use Window reference instead. + var topLevel = TopLevel.GetTopLevel(_target) ?? throw new ArgumentNullException(nameof(_target), "Window Target."); + + // Start async operation to open the dialog. + var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions + { + AllowMultiple = false, + Title = "Select a Folder", + }); + + var folder = folders.SingleOrDefault(); + + return folder?.TryGetLocalPath() ?? throw new ArgumentException("Could not load local path."); + } +} \ No newline at end of file diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 510891f..1573c76 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -6,9 +6,12 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Platform.Storage; using FluentAvalonia.UI.Data; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SharpFM.Models; +using SharpFM.Services; namespace SharpFM.ViewModels; @@ -18,6 +21,8 @@ public partial class MainWindowViewModel : INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; + public string? CurrentPath = null; + private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); @@ -27,10 +32,17 @@ public MainWindowViewModel(ILogger logger) { _logger = logger; - var clipContext = new ClipRepository(); + FileMakerClips = []; + + LoadClips(CurrentPath); + } + + private void LoadClips(string? pathToLoad) + { + var clipContext = new ClipRepository(pathToLoad); clipContext.LoadClips(); - FileMakerClips = []; + FileMakerClips.Clear(); foreach (var clip in clipContext.Clips) { @@ -46,9 +58,18 @@ public MainWindowViewModel(ILogger logger) } } - public void SaveToDb() + public async Task OpenFolderPicker() + { + var folderService = App.Current?.Services?.GetService(); + + CurrentPath = await folderService.GetFolderAsync(); + + LoadClips(CurrentPath); + } + + public void SaveClipsStorage() { - var clipContext = new ClipRepository(); + var clipContext = new ClipRepository(CurrentPath); var dbClips = clipContext.Clips.ToList(); From 4bf456642750804db40e17bfec9612f3a85b1099 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Tue, 27 Feb 2024 20:36:09 -0600 Subject: [PATCH 3/6] chore: cleanup unused clipId --- Models/Clip.cs | 5 ----- ViewModels/ClipViewModel.cs | 16 +--------------- ViewModels/MainWindowViewModel.cs | 13 ++++++------- 3 files changed, 7 insertions(+), 27 deletions(-) diff --git a/Models/Clip.cs b/Models/Clip.cs index caa727a..dc0ca56 100644 --- a/Models/Clip.cs +++ b/Models/Clip.cs @@ -5,11 +5,6 @@ namespace SharpFM.Models; /// public class Clip { - /// - /// Database Id - /// - public int ClipId { get; set; } - /// /// Display name for clip may match Name inside the xml data or may not. /// diff --git a/ViewModels/ClipViewModel.cs b/ViewModels/ClipViewModel.cs index 9f0b301..82e5b9e 100644 --- a/ViewModels/ClipViewModel.cs +++ b/ViewModels/ClipViewModel.cs @@ -14,23 +14,9 @@ private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") public FileMakerClip Clip { get; set; } - public ClipViewModel(FileMakerClip clip) : this(clip, null) { } - - public ClipViewModel(FileMakerClip clip, int? clipId) + public ClipViewModel(FileMakerClip clip) { Clip = clip; - ClipId = clipId; - } - - private int? _clipId; - public int? ClipId - { - get => _clipId; - set - { - _clipId = value; - NotifyPropertyChanged(); - } } public string ClipType diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 1573c76..9d3a0d0 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Platform.Storage; using FluentAvalonia.UI.Data; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -51,8 +50,7 @@ private void LoadClips(string? pathToLoad) clip.ClipName, clip.ClipType, clip.ClipXml - ), - clip.ClipId + ) ) ); } @@ -60,11 +58,12 @@ private void LoadClips(string? pathToLoad) public async Task OpenFolderPicker() { - var folderService = App.Current?.Services?.GetService(); - - CurrentPath = await folderService.GetFolderAsync(); + if (App.Current?.Services?.GetService() is FolderService folderService) + { + CurrentPath = await folderService.GetFolderAsync(); - LoadClips(CurrentPath); + LoadClips(CurrentPath); + } } public void SaveClipsStorage() From 899d46a99709849197bd9cf98419498911d23949 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Wed, 28 Feb 2024 20:15:42 -0600 Subject: [PATCH 4/6] fix: reorganize menu items --- MainWindow.axaml | 24 ++++++++++++++++++------ ViewModels/MainWindowViewModel.cs | 14 ++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/MainWindow.axaml b/MainWindow.axaml index 5b69ed6..aa3fce5 100644 --- a/MainWindow.axaml +++ b/MainWindow.axaml @@ -19,30 +19,41 @@ - + + + + - + + + + - - - - + + + + + + + + @@ -63,6 +74,7 @@ _currentPath; + set + { + _currentPath = value; + NotifyPropertyChanged(); + } + } + } \ No newline at end of file From b68954ed8d40b7cd69c34f44d94c148b568276c2 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Sat, 2 Mar 2024 10:26:08 -0600 Subject: [PATCH 5/6] chore: move clip path default location to vm. --- Models/ClipRepository.cs | 8 +------- ViewModels/MainWindowViewModel.cs | 8 +++++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Models/ClipRepository.cs b/Models/ClipRepository.cs index e26a436..37cc577 100644 --- a/Models/ClipRepository.cs +++ b/Models/ClipRepository.cs @@ -22,14 +22,8 @@ public class ClipRepository /// /// Constructor. /// - public ClipRepository(string? path = null) + public ClipRepository(string path) { - // default to the local app data folder + \SharpFM, otherwise use provided path - path ??= Path.Join( - path1: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - path2: "SharpFM" - ); - // ensure the directory exists if (!Directory.Exists(path)) { diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 8128536..6ef9c19 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.ComponentModel; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -28,13 +29,18 @@ private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") public MainWindowViewModel(ILogger logger) { _logger = logger; + // default to the local app data folder + \SharpFM, otherwise use provided path + _currentPath ??= Path.Join( + path1: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + path2: "SharpFM" + ); FileMakerClips = []; LoadClips(CurrentPath); } - private void LoadClips(string? pathToLoad) + private void LoadClips(string pathToLoad) { var clipContext = new ClipRepository(pathToLoad); clipContext.LoadClips(); From 65b7b3217fbcef555a1a3ed8fa0848be905cb158 Mon Sep 17 00:00:00 2001 From: Nate Bross Date: Fri, 8 Mar 2024 19:32:48 -0600 Subject: [PATCH 6/6] fix: move path to sub folder --- .vscode/settings.json | 1 + ViewModels/MainWindowViewModel.cs | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 00609b4..2957c8f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,6 +24,7 @@ "filemaker", "fmxmlsnippet", "Mvvm", + "nlog", "xdoc", "XMFD", "XMSC", diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 6ef9c19..f47ff51 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -32,7 +32,7 @@ public MainWindowViewModel(ILogger logger) // default to the local app data folder + \SharpFM, otherwise use provided path _currentPath ??= Path.Join( path1: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - path2: "SharpFM" + path2: Path.Join("SharpFM", "Clips") ); FileMakerClips = []; @@ -74,11 +74,11 @@ public void SaveClipsStorage() { var clipContext = new ClipRepository(CurrentPath); - var dbClips = clipContext.Clips.ToList(); + var fsClips = clipContext.Clips.ToList(); foreach (var clip in FileMakerClips) { - var dbClip = dbClips.FirstOrDefault(dbc => dbc.ClipName == clip.Name); + var dbClip = fsClips.FirstOrDefault(dbc => dbc.ClipName == clip.Name); if (dbClip is not null) { @@ -99,6 +99,7 @@ public void SaveClipsStorage() clipContext.SaveChanges(); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Bound to Xaml Button, throws when static.")] public void ExitApplication() { if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp)