├── .gitignore ├── LICENSE ├── MatoEditor.sln ├── MatoEditor.sln.DotSettings.user ├── MatoEditor ├── App.axaml ├── App.axaml.cs ├── Assets │ ├── B.svg │ ├── Code.svg │ ├── CodeBlock.svg │ ├── Dark.svg │ ├── Del.svg │ ├── Directory.svg │ ├── Editor.svg │ ├── EditorViewer.svg │ ├── File.svg │ ├── H1.svg │ ├── H2.svg │ ├── H3.svg │ ├── H4.svg │ ├── H5.svg │ ├── H6.svg │ ├── I.svg │ ├── Light.svg │ ├── Line.svg │ ├── OList.svg │ ├── Open.svg │ ├── Path.svg │ ├── Point.svg │ ├── Quote.svg │ ├── Save.svg │ ├── Table.svg │ ├── Tomato.ico │ ├── UList.svg │ ├── Viewer.svg │ └── X.svg ├── Dialogs │ ├── BaseDialog.axaml │ ├── BaseDialog.axaml.cs │ ├── BaseDialogViewModel.cs │ ├── TextBoxDialog.axaml │ ├── TextBoxDialog.axaml.cs │ ├── TextBoxDialogViewModel.cs │ ├── TwoNumberBoxDialog.axaml │ ├── TwoNumberBoxDialog.axaml.cs │ └── TwoNumberBoxDialogViewModel.cs ├── MatoEditor.csproj ├── Models │ ├── FileTab.cs │ └── Node.cs ├── Program.cs ├── Services │ ├── ConfigurationService.cs │ ├── FileSystemService.cs │ ├── IFileSystemService.cs │ └── StorageService.cs ├── Styles │ └── Styles.axaml ├── Utils │ ├── AvalonEditBehavior.cs │ └── Converters │ │ ├── FileTabConvert.cs │ │ └── SvgConvert.cs ├── ViewLocator.cs ├── ViewModels │ ├── DocumentTreeViewModel.cs │ ├── EditorViewModel.cs │ ├── MainWindowViewModel.cs │ ├── NavigationViewModel.cs │ └── ViewModelBase.cs ├── Views │ ├── DocumentTree.axaml │ ├── DocumentTree.axaml.cs │ ├── Editor.axaml │ ├── Editor.axaml.cs │ ├── MainWindow.axaml │ ├── MainWindow.axaml.cs │ ├── Navigation.axaml │ └── Navigation.axaml.cs └── app.manifest └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, 酷酷番茄 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MatoEditor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatoEditor", "MatoEditor\MatoEditor.csproj", "{C2780E8D-04EA-4AAA-968A-D02F412EABE8}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {C2780E8D-04EA-4AAA-968A-D02F412EABE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {C2780E8D-04EA-4AAA-968A-D02F412EABE8}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {C2780E8D-04EA-4AAA-968A-D02F412EABE8}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {C2780E8D-04EA-4AAA-968A-D02F412EABE8}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /MatoEditor.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 |  2 | ForceIncluded 3 | ForceIncluded 4 | ForceIncluded 5 | ForceIncluded 6 | ForceIncluded 7 | ForceIncluded 8 | ForceIncluded 9 | ForceIncluded 10 | ForceIncluded 11 | ForceIncluded 12 | ForceIncluded -------------------------------------------------------------------------------- /MatoEditor/App.axaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /MatoEditor/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Controls; 4 | using Avalonia.Controls.ApplicationLifetimes; 5 | using Avalonia.Data.Core.Plugins; 6 | using Avalonia.Markup.Xaml; 7 | using Avalonia.Styling; 8 | using MatoEditor.Services; 9 | using MatoEditor.ViewModels; 10 | using MatoEditor.Views; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace MatoEditor; 14 | 15 | public partial class App : Application 16 | { 17 | public override void Initialize() 18 | { 19 | AvaloniaXamlLoader.Load(this); 20 | } 21 | 22 | public IServiceProvider? ServiceProvider { get; private set; } 23 | 24 | public override void OnFrameworkInitializationCompleted() 25 | { 26 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 27 | { 28 | BindingPlugins.DataValidators.RemoveAt(0); 29 | 30 | var services = new ServiceCollection(); 31 | ConfigureServices(services); 32 | ServiceProvider = services.BuildServiceProvider(); 33 | 34 | var configService = ServiceProvider.GetRequiredService(); 35 | configService.LoadConfiguration(); 36 | 37 | var mainWindow = new MainWindow(); 38 | var viewModel = ActivatorUtilities.CreateInstance( 39 | ServiceProvider, 40 | ServiceProvider.GetRequiredService(), 41 | ServiceProvider.GetRequiredService(), 42 | mainWindow 43 | ); 44 | mainWindow.DataContext = viewModel; 45 | desktop.MainWindow = mainWindow; 46 | } 47 | 48 | base.OnFrameworkInitializationCompleted(); 49 | } 50 | 51 | private void ConfigureServices(IServiceCollection services) 52 | { 53 | services.AddSingleton(); 54 | services.AddSingleton(); 55 | services.AddSingleton(); 56 | services.AddTransient(); 57 | } 58 | } -------------------------------------------------------------------------------- /MatoEditor/Assets/B.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/Code.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/CodeBlock.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Dark.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Del.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Directory.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Editor.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/EditorViewer.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/File.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/H1.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/H2.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/H3.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/H4.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/H5.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/H6.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/I.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/Light.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Line.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MatoEditor/Assets/OList.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Open.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Path.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Point.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MatoEditor/Assets/Quote.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Save.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Table.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Tomato.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoolCoolTomato/MatoEditor/6b99273e09eac715363564d85053ddd0dfb5752f/MatoEditor/Assets/Tomato.ico -------------------------------------------------------------------------------- /MatoEditor/Assets/UList.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/Viewer.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /MatoEditor/Assets/X.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MatoEditor/Dialogs/BaseDialog.axaml: -------------------------------------------------------------------------------- 1 |  10 | 14 | 15 | -------------------------------------------------------------------------------- /MatoEditor/Dialogs/BaseDialog.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace MatoEditor.Dialogs; 6 | 7 | public partial class BaseDialog : UserControl 8 | { 9 | public BaseDialog() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /MatoEditor/Dialogs/BaseDialogViewModel.cs: -------------------------------------------------------------------------------- 1 | using MatoEditor.ViewModels; 2 | 3 | namespace MatoEditor.Dialogs; 4 | 5 | public class BaseDialogViewModel : ViewModelBase 6 | { 7 | public BaseDialogViewModel() {} 8 | } -------------------------------------------------------------------------------- /MatoEditor/Dialogs/TextBoxDialog.axaml: -------------------------------------------------------------------------------- 1 |  10 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /MatoEditor/Dialogs/TextBoxDialog.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace MatoEditor.Dialogs; 6 | 7 | public partial class TextBoxDialog : UserControl 8 | { 9 | public TextBoxDialog() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /MatoEditor/Dialogs/TextBoxDialogViewModel.cs: -------------------------------------------------------------------------------- 1 | using MatoEditor.ViewModels; 2 | 3 | namespace MatoEditor.Dialogs; 4 | 5 | public class TextBoxDialogViewModel : ViewModelBase 6 | { 7 | public string Content { get; set; } 8 | public TextBoxDialogViewModel() 9 | { 10 | Content = ""; 11 | } 12 | } -------------------------------------------------------------------------------- /MatoEditor/Dialogs/TwoNumberBoxDialog.axaml: -------------------------------------------------------------------------------- 1 |  10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /MatoEditor/Dialogs/TwoNumberBoxDialog.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace MatoEditor.Dialogs; 6 | 7 | public partial class TwoNumberBoxDialog : UserControl 8 | { 9 | public TwoNumberBoxDialog() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /MatoEditor/Dialogs/TwoNumberBoxDialogViewModel.cs: -------------------------------------------------------------------------------- 1 | using MatoEditor.ViewModels; 2 | 3 | namespace MatoEditor.Dialogs; 4 | 5 | public class TwoNumberBoxDialogViewModel : ViewModelBase 6 | { 7 | public string Label1 { get; set; } 8 | public string Label2 { get; set; } 9 | public int Number1 { get; set; } 10 | public int Number2 { get; set; } 11 | 12 | public TwoNumberBoxDialogViewModel() 13 | { 14 | Label1 = ""; 15 | Label2 = ""; 16 | Number1 = 1; 17 | Number2 = 1; 18 | } 19 | } -------------------------------------------------------------------------------- /MatoEditor/MatoEditor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | net9.0 5 | enable 6 | true 7 | app.manifest 8 | true 9 | Assets\Tomato.ico 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MatoEditor/Models/FileTab.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace MatoEditor.Models; 4 | 5 | public class FileTab : ReactiveObject 6 | { 7 | public FileTab() 8 | { 9 | Name = ""; 10 | Path = ""; 11 | OldContentString = ""; 12 | NewContentString = ""; 13 | } 14 | private string _name; 15 | public string Name 16 | { 17 | get => _name; 18 | set => this.RaiseAndSetIfChanged(ref _name, value); 19 | } 20 | private string _path; 21 | public string Path 22 | { 23 | get => _path; 24 | set => this.RaiseAndSetIfChanged(ref _path, value); 25 | } 26 | 27 | private string _oldContentString; 28 | public string OldContentString 29 | { 30 | get => _oldContentString; 31 | set => this.RaiseAndSetIfChanged(ref _oldContentString, value); 32 | } 33 | 34 | private string _newContentString; 35 | public string NewContentString 36 | { 37 | get => _newContentString; 38 | set => this.RaiseAndSetIfChanged(ref _newContentString, value); 39 | } 40 | } -------------------------------------------------------------------------------- /MatoEditor/Models/Node.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using ReactiveUI; 3 | 4 | namespace MatoEditor.Models; 5 | 6 | public class Node : ReactiveObject 7 | { 8 | public Node() 9 | { 10 | Name = ""; 11 | Path = ""; 12 | IsDirectory = false; 13 | SubNodes = new ObservableCollection(); 14 | } 15 | private string _name; 16 | public string Name 17 | { 18 | get => _name; 19 | set => this.RaiseAndSetIfChanged(ref _name, value); 20 | } 21 | private string _path; 22 | public string Path 23 | { 24 | get => _path; 25 | set => this.RaiseAndSetIfChanged(ref _path, value); 26 | } 27 | private bool _isDirectory; 28 | public bool IsDirectory 29 | { 30 | get => _isDirectory; 31 | set => this.RaiseAndSetIfChanged(ref _isDirectory, value); 32 | } 33 | private ObservableCollection _subNodes; 34 | public ObservableCollection SubNodes 35 | { 36 | get => _subNodes; 37 | set => this.RaiseAndSetIfChanged(ref _subNodes, value); 38 | } 39 | } -------------------------------------------------------------------------------- /MatoEditor/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | using System; 4 | 5 | namespace MatoEditor; 6 | 7 | sealed class Program 8 | { 9 | // Initialization code. Don't use any Avalonia, third-party APIs or any 10 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 11 | // yet and stuff might break. 12 | [STAThread] 13 | public static void Main(string[] args) => BuildAvaloniaApp() 14 | .StartWithClassicDesktopLifetime(args); 15 | 16 | // Avalonia configuration, don't remove; also used by visual designer. 17 | public static AppBuilder BuildAvaloniaApp() 18 | => AppBuilder.Configure() 19 | .UsePlatformDetect() 20 | .WithInterFont() 21 | .LogToTrace() 22 | .UseReactiveUI(); 23 | } -------------------------------------------------------------------------------- /MatoEditor/Services/ConfigurationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | using Avalonia; 5 | using Avalonia.Styling; 6 | using ReactiveUI; 7 | 8 | namespace MatoEditor.Services; 9 | 10 | public class ConfigurationService : ReactiveObject 11 | { 12 | private const string ConfigFileName = "config.json"; 13 | private readonly string _configFilePath; 14 | private readonly StorageService _storageService; 15 | 16 | public ConfigurationService(StorageService storageService) 17 | { 18 | _storageService = storageService; 19 | _configFilePath = Path.Combine( 20 | Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), 21 | ".MatoEditor", 22 | ConfigFileName 23 | ); 24 | } 25 | 26 | public void LoadConfiguration() 27 | { 28 | if (!File.Exists(_configFilePath)) 29 | { 30 | CreateDefaultConfig(); 31 | } 32 | 33 | var json = File.ReadAllText(_configFilePath); 34 | var config = JsonSerializer.Deserialize(json); 35 | 36 | if (config != null && !string.IsNullOrEmpty(config.LastOpenedDirectory)) 37 | { 38 | _storageService.RootDirectoryPath = config.LastOpenedDirectory; 39 | Application.Current.RequestedThemeVariant = 40 | config.Theme == "Light" ? ThemeVariant.Light : ThemeVariant.Dark; 41 | } 42 | } 43 | 44 | public void SaveConfiguration() 45 | { 46 | var config = new EditorConfig 47 | { 48 | LastOpenedDirectory = _storageService.RootDirectoryPath, 49 | Theme = Application.Current.RequestedThemeVariant == ThemeVariant.Light ? "Light" : "Dark" 50 | }; 51 | 52 | var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); 53 | Directory.CreateDirectory(Path.GetDirectoryName(_configFilePath)); 54 | File.WriteAllText(_configFilePath, json); 55 | } 56 | 57 | private void CreateDefaultConfig() 58 | { 59 | var defaultConfig = new EditorConfig 60 | { 61 | LastOpenedDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), 62 | Theme = Application.Current.RequestedThemeVariant == ThemeVariant.Light ? "Light" : "Dark" 63 | }; 64 | 65 | var json = JsonSerializer.Serialize(defaultConfig, new JsonSerializerOptions { WriteIndented = true }); 66 | Directory.CreateDirectory(Path.GetDirectoryName(_configFilePath)); 67 | File.WriteAllText(_configFilePath, json); 68 | } 69 | } 70 | 71 | public class EditorConfig 72 | { 73 | public string LastOpenedDirectory { get; set; } 74 | public string Theme { get; set; } 75 | } 76 | -------------------------------------------------------------------------------- /MatoEditor/Services/FileSystemService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace MatoEditor.Services; 8 | 9 | public class FileSystemService : IFileSystemService 10 | { 11 | public async Task> GetSubDirectories(string path) 12 | { 13 | try 14 | { 15 | return await Task.Run(() => 16 | { 17 | var directory = new DirectoryInfo(path); 18 | return directory.EnumerateDirectories(); 19 | }); 20 | } 21 | catch (Exception) 22 | { 23 | return Enumerable.Empty(); 24 | } 25 | } 26 | public async Task> GetFiles(string path) 27 | { 28 | try 29 | { 30 | return await Task.Run(() => 31 | { 32 | var directory = new DirectoryInfo(path); 33 | return directory.EnumerateFiles(); 34 | }); 35 | } 36 | catch (Exception) 37 | { 38 | return Enumerable.Empty(); 39 | } 40 | } 41 | 42 | public async Task DirectoryExistsAsync(string path) 43 | { 44 | try 45 | { 46 | return await Task.FromResult(Directory.Exists(path)); 47 | } 48 | catch (Exception) 49 | { 50 | return false; 51 | } 52 | } 53 | public async Task CreateDirectoryAsync(string path) 54 | { 55 | try 56 | { 57 | await Task.Run(() => Directory.CreateDirectory(path)); 58 | return true; 59 | } 60 | catch (Exception) 61 | { 62 | return false; 63 | } 64 | } 65 | public async Task RenameDirectoryAsync(string oldPath, string newPath) 66 | { 67 | try 68 | { 69 | await Task.Run(() => Directory.Move(oldPath, newPath)); 70 | return true; 71 | } 72 | catch (Exception) 73 | { 74 | return false; 75 | } 76 | } 77 | public async Task DeleteDirectoryAsync(string path) 78 | { 79 | try 80 | { 81 | await Task.Run(() => Directory.Delete(path, true)); 82 | return true; 83 | } 84 | catch (Exception) 85 | { 86 | return false; 87 | } 88 | } 89 | 90 | public async Task FileExistsAsync(string path) 91 | { 92 | try 93 | { 94 | return await Task.FromResult(File.Exists(path)); 95 | } 96 | catch (Exception) 97 | { 98 | return false; 99 | } 100 | } 101 | public async Task CreateFileAsync(string path) 102 | { 103 | try 104 | { 105 | if (await FileExistsAsync(path)) 106 | { 107 | return false; 108 | } 109 | Directory.CreateDirectory(Path.GetDirectoryName(path)); 110 | await using (File.Create(path)){} 111 | return true; 112 | } 113 | catch (Exception) 114 | { 115 | return false; 116 | } 117 | } 118 | public async Task RenameFileAsync(string oldPath, string newPath) 119 | { 120 | try 121 | { 122 | if (!(await FileExistsAsync(oldPath)) || await FileExistsAsync(newPath)) 123 | { 124 | return false; 125 | } 126 | Directory.CreateDirectory(Path.GetDirectoryName(newPath)); 127 | File.Move(oldPath, newPath); 128 | return true; 129 | } 130 | catch (Exception) 131 | { 132 | return false; 133 | } 134 | } 135 | public async Task DeleteFileAsync(string path) 136 | { 137 | try 138 | { 139 | if (!(await FileExistsAsync(path))) 140 | { 141 | return false; 142 | } 143 | File.Delete(path); 144 | return true; 145 | } 146 | catch (Exception) 147 | { 148 | return false; 149 | } 150 | } 151 | public async Task ReadFileAsync(string path) 152 | { 153 | try 154 | { 155 | using var reader = new StreamReader(path); 156 | return await reader.ReadToEndAsync(); 157 | } 158 | catch (Exception) 159 | { 160 | return string.Empty; 161 | } 162 | } 163 | public async Task WriteFileAsync(string path, string content) 164 | { 165 | try 166 | { 167 | await using var writer = new StreamWriter(path, false); 168 | await writer.WriteAsync(content); 169 | return true; 170 | } 171 | catch (Exception) 172 | { 173 | return false; 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /MatoEditor/Services/IFileSystemService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace MatoEditor.Services; 6 | 7 | public interface IFileSystemService 8 | { 9 | Task> GetSubDirectories(string path); 10 | Task> GetFiles(string path); 11 | 12 | Task DirectoryExistsAsync(string path); 13 | Task CreateDirectoryAsync(string path); 14 | Task RenameDirectoryAsync(string oldPath, string newPath); 15 | Task DeleteDirectoryAsync(string path); 16 | 17 | Task FileExistsAsync(string path); 18 | Task CreateFileAsync(string path); 19 | Task RenameFileAsync(string oldPath, string newPath); 20 | Task DeleteFileAsync(string path); 21 | Task ReadFileAsync(string path); 22 | Task WriteFileAsync(string path, string content); 23 | } -------------------------------------------------------------------------------- /MatoEditor/Services/StorageService.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace MatoEditor.Services; 4 | 5 | public class StorageService : ReactiveObject 6 | { 7 | private string _rootDirectoryPath; 8 | public string RootDirectoryPath 9 | { 10 | get => _rootDirectoryPath; 11 | set => this.RaiseAndSetIfChanged(ref _rootDirectoryPath, value); 12 | } 13 | 14 | private string _currentFilePath; 15 | public string CurrentFilePath 16 | { 17 | get => _currentFilePath; 18 | set => this.RaiseAndSetIfChanged(ref _currentFilePath, value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MatoEditor/Styles/Styles.axaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | #FFFFFF 15 | #F8F8F8 16 | #F0F0F0 17 | #E8E8E8 18 | #E0E0E0 19 | #202020 20 | 21 | 22 | #101010 23 | #181818 24 | #202020 25 | #282828 26 | #303030 27 | #F0F0F0 28 | 29 | 30 | 31 | 32 | 33 | 37 | 40 | 41 | 46 | 49 | 50 | 53 | 54 | 57 | 60 | 63 | 66 | 67 | 70 | 73 | 74 | 80 | 81 | 85 | 91 | 92 | 97 | 102 | 107 | 112 | 117 | 122 | 123 | 126 | 129 | 132 | 133 | 137 | 140 | 143 | 144 | 149 | 152 | 157 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /MatoEditor/Utils/AvalonEditBehavior.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia; 3 | using Avalonia.Xaml.Interactivity; 4 | using AvaloniaEdit; 5 | 6 | namespace MatoEditor.Utils; 7 | 8 | public sealed class AvalonEditBehavior : Behavior 9 | { 10 | private TextEditor _textEditor = null; 11 | 12 | public static readonly StyledProperty TextProperty = 13 | AvaloniaProperty.Register(nameof(Text)); 14 | 15 | public string Text 16 | { 17 | get => GetValue(TextProperty); 18 | set => SetValue(TextProperty, value); 19 | } 20 | 21 | protected override void OnAttached() 22 | { 23 | base.OnAttached(); 24 | 25 | if (AssociatedObject is TextEditor textEditor) 26 | { 27 | _textEditor = textEditor; 28 | _textEditor.TextChanged += TextChanged; 29 | this.GetObservable(TextProperty).Subscribe(TextPropertyChanged); 30 | } 31 | } 32 | 33 | protected override void OnDetaching() 34 | { 35 | base.OnDetaching(); 36 | 37 | if (_textEditor != null) 38 | { 39 | _textEditor.TextChanged -= TextChanged; 40 | } 41 | } 42 | 43 | private void TextChanged(object sender, EventArgs eventArgs) 44 | { 45 | if (_textEditor != null && _textEditor.Document != null) 46 | { 47 | Text = _textEditor.Document.Text; 48 | } 49 | } 50 | 51 | private void TextPropertyChanged(string text) 52 | { 53 | if (_textEditor != null && _textEditor.Document != null && text != null) 54 | { 55 | var caretOffset = _textEditor.CaretOffset; 56 | _textEditor.Document.Text = text; 57 | if (caretOffset > _textEditor.Document.TextLength) 58 | { 59 | _textEditor.CaretOffset = _textEditor.Document.TextLength; 60 | } 61 | else 62 | { 63 | _textEditor.CaretOffset = caretOffset; 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /MatoEditor/Utils/Converters/FileTabConvert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using Avalonia; 5 | using Avalonia.Data.Converters; 6 | 7 | namespace MatoEditor.Utils.Converters; 8 | 9 | public class FileTabConverter : IMultiValueConverter 10 | { 11 | public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | if (values[0] is string oldContent && values[1] is string newContent) 14 | { 15 | return oldContent != newContent; 16 | } 17 | return AvaloniaProperty.UnsetValue; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MatoEditor/Utils/Converters/SvgConvert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Text.RegularExpressions; 5 | using Avalonia; 6 | using Avalonia.Data.Converters; 7 | using Avalonia.Media; 8 | using Avalonia.Platform; 9 | 10 | namespace MatoEditor.Utils.Converters; 11 | 12 | public class SvgConverter : IValueConverter 13 | { 14 | public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 15 | { 16 | if (value is string stringValue) 17 | { 18 | var uri = stringValue; 19 | 20 | var stream = AssetLoader.Open(new Uri(uri)); 21 | using (var reader = new StreamReader(stream)) 22 | { 23 | var svgContent = reader.ReadToEnd(); 24 | return StreamGeometry.Parse(ExtractSvgPath(svgContent)); 25 | } 26 | } 27 | return AvaloniaProperty.UnsetValue; 28 | } 29 | 30 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 31 | { 32 | return AvaloniaProperty.UnsetValue; 33 | } 34 | 35 | private static string ExtractSvgPath(string svgContent) 36 | { 37 | string pattern = @"]*\s+d=""([^""]+)""[^>]*>"; 38 | Regex regex = new Regex(pattern); 39 | Match match = regex.Match(svgContent); 40 | if (match.Success) 41 | { 42 | return match.Groups[1].Value; 43 | } 44 | return string.Empty; 45 | } 46 | } -------------------------------------------------------------------------------- /MatoEditor/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Templates; 4 | using MatoEditor.ViewModels; 5 | 6 | namespace MatoEditor; 7 | 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public Control? Build(object? data) 11 | { 12 | if (data is null) 13 | return null; 14 | 15 | var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); 16 | var type = Type.GetType(name); 17 | 18 | if (type != null) 19 | { 20 | var control = (Control)Activator.CreateInstance(type)!; 21 | control.DataContext = data; 22 | return control; 23 | } 24 | 25 | return new TextBlock { Text = "Not Found: " + name }; 26 | } 27 | 28 | public bool Match(object? data) 29 | { 30 | return data is ViewModelBase; 31 | } 32 | } -------------------------------------------------------------------------------- /MatoEditor/ViewModels/DocumentTreeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Windows.Input; 7 | using Avalonia.Controls; 8 | using Avalonia.Data.Converters; 9 | using MatoEditor.Dialogs; 10 | using MatoEditor.Models; 11 | using MatoEditor.Services; 12 | using ReactiveUI; 13 | using Ursa.Controls; 14 | 15 | namespace MatoEditor.ViewModels; 16 | 17 | public class DocumentTreeViewModel : ViewModelBase 18 | { 19 | public DocumentTreeViewModel(IFileSystemService fileSystemService, StorageService storageService) 20 | { 21 | _fileSystemService = fileSystemService; 22 | _storageService = storageService; 23 | 24 | _rootNode = new Node(); 25 | _storageService.WhenAnyValue(x => x.RootDirectoryPath) 26 | .Subscribe(RootDirectoryPath => 27 | { 28 | _rootNode.Name = Path.GetFileName(RootDirectoryPath); 29 | _rootNode.Path = RootDirectoryPath; 30 | _rootNode.SubNodes.Clear(); 31 | _rootNode.IsDirectory = true; 32 | InitDocumentTree(); 33 | }); 34 | SelectedNode = new Node(); 35 | 36 | OpenCreateDirectoryDialogCommand = ReactiveCommand.Create(OpenCreateDirectoryDialog); 37 | OpenRenameDirectoryDialogCommand = ReactiveCommand.Create(OpenRenameDirectoryDialog); 38 | OpenDeleteDirectoryDialogCommand = ReactiveCommand.Create(OpenDeleteDirectoryDialog); 39 | OpenCreateFileDialogCommand = ReactiveCommand.Create(OpenCreateFileDialog); 40 | OpenRenameFileDialogCommand = ReactiveCommand.Create(OpenRenameFileDialog); 41 | OpenDeleteFileDialogCommand = ReactiveCommand.Create(OpenDeleteFileDialog); 42 | 43 | this.WhenAnyValue(x => x.SelectedNode.Path) 44 | .Subscribe(_ => ChangeSelectedFile()); 45 | } 46 | private readonly IFileSystemService _fileSystemService; 47 | private StorageService _storageService; 48 | 49 | private Node _rootNode { get; set; } 50 | 51 | private ObservableCollection _documentTree; 52 | public ObservableCollection DocumentTree 53 | { 54 | get => _documentTree; 55 | set => this.RaiseAndSetIfChanged(ref _documentTree, value); 56 | } 57 | 58 | private Node _selectedNode; 59 | public Node SelectedNode 60 | { 61 | get => _selectedNode; 62 | set => this.RaiseAndSetIfChanged(ref _selectedNode, value); 63 | } 64 | 65 | public ICommand OpenCreateDirectoryDialogCommand { get; } 66 | public ICommand OpenRenameDirectoryDialogCommand { get; } 67 | public ICommand OpenDeleteDirectoryDialogCommand { get; } 68 | public ICommand OpenCreateFileDialogCommand { get; } 69 | public ICommand OpenRenameFileDialogCommand { get; } 70 | public ICommand OpenDeleteFileDialogCommand { get; } 71 | 72 | private void InitDocumentTree() 73 | { 74 | BuildDocumentTree(_rootNode); 75 | DocumentTree = new ObservableCollection() { _rootNode }; 76 | } 77 | private async void BuildDocumentTree(Node node) 78 | { 79 | IEnumerable subDirectoryInfos = await _fileSystemService.GetSubDirectories(node.Path); 80 | foreach (var subDirectoryInfo in subDirectoryInfos) 81 | { 82 | var subDirectory = new Node() 83 | { 84 | Name = subDirectoryInfo.Name, 85 | Path = subDirectoryInfo.FullName, 86 | IsDirectory = true, 87 | }; 88 | node.SubNodes.Add(subDirectory); 89 | BuildDocumentTree(subDirectory); 90 | } 91 | 92 | IEnumerable fileInfos = await _fileSystemService.GetFiles(node.Path); 93 | foreach (var fileInfo in fileInfos) 94 | { 95 | var file = new Node() 96 | { 97 | Name = fileInfo.Name, 98 | Path = fileInfo.FullName, 99 | IsDirectory = false, 100 | }; 101 | node.SubNodes.Add(file); 102 | } 103 | } 104 | 105 | private void ChangeSelectedFile() 106 | { 107 | if (!SelectedNode.IsDirectory) 108 | { 109 | _storageService.CurrentFilePath = SelectedNode.Path; 110 | } 111 | } 112 | 113 | private Node FindNodeByPath(Node currentNode, string path) 114 | { 115 | if (currentNode.Path == path) 116 | { 117 | return currentNode; 118 | } 119 | 120 | foreach (var subNode in currentNode.SubNodes) 121 | { 122 | var result = FindNodeByPath(subNode, path); 123 | if (result != null) 124 | { 125 | return result; 126 | } 127 | } 128 | 129 | return null; 130 | } 131 | private void InsertNode(Node currentNode, Node node) 132 | { 133 | int insertIndex = -1; 134 | for (int i = 0; i < currentNode.SubNodes.Count; i++) 135 | { 136 | if (currentNode.SubNodes[i].IsDirectory == node.IsDirectory) 137 | { 138 | if (string.Compare(currentNode.SubNodes[i].Name, node.Name, StringComparison.OrdinalIgnoreCase) > 0) 139 | { 140 | insertIndex = i; 141 | break; 142 | } 143 | } 144 | 145 | if (!currentNode.SubNodes[i].IsDirectory && node.IsDirectory) 146 | { 147 | insertIndex = i; 148 | break; 149 | } 150 | } 151 | if (insertIndex == -1) 152 | { 153 | currentNode.SubNodes.Add(node); 154 | } 155 | else 156 | { 157 | currentNode.SubNodes.Insert(insertIndex, node); 158 | } 159 | } 160 | private bool DeleteNodeByPath(Node currentNode, string path) 161 | { 162 | for (int i = 0; i < currentNode.SubNodes.Count; i++) 163 | { 164 | var subNode = currentNode.SubNodes[i]; 165 | if (subNode.Path == path) 166 | { 167 | currentNode.SubNodes.RemoveAt(i); 168 | return true; 169 | } 170 | 171 | var result = DeleteNodeByPath(subNode, path); 172 | if (result) 173 | { 174 | return true; 175 | } 176 | } 177 | 178 | return false; 179 | } 180 | 181 | private async void OpenCreateDirectoryDialog(string path) 182 | { 183 | var options = new DialogOptions() 184 | { 185 | Title = "Input directory name", 186 | Mode = DialogMode.None, 187 | Button = DialogButton.OKCancel, 188 | ShowInTaskBar = false, 189 | IsCloseButtonVisible = true, 190 | StartupLocation = WindowStartupLocation.CenterOwner, 191 | }; 192 | var textBoxDialogViewModel = new TextBoxDialogViewModel(); 193 | var result = await Dialog.ShowModal(textBoxDialogViewModel, options: options); 194 | if (result == DialogResult.OK) 195 | { 196 | var currentNode = FindNodeByPath(_rootNode, path); 197 | if (currentNode != null) 198 | { 199 | var directory = new Node() 200 | { 201 | Name = textBoxDialogViewModel.Content, 202 | Path = path + "/" + textBoxDialogViewModel.Content, 203 | IsDirectory = true, 204 | }; 205 | InsertNode(currentNode, directory); 206 | await _fileSystemService.CreateDirectoryAsync(directory.Path); 207 | } 208 | } 209 | } 210 | private async void OpenRenameDirectoryDialog(Node node) 211 | { 212 | var options = new DialogOptions() 213 | { 214 | Title = "Input new directory name", 215 | Mode = DialogMode.None, 216 | Button = DialogButton.OKCancel, 217 | ShowInTaskBar = false, 218 | IsCloseButtonVisible = true, 219 | StartupLocation = WindowStartupLocation.CenterOwner, 220 | }; 221 | var textBoxDialogViewModel = new TextBoxDialogViewModel() 222 | { 223 | Content = node.Name, 224 | }; 225 | var result = await Dialog.ShowModal(textBoxDialogViewModel, options: options); 226 | if (result == DialogResult.OK) 227 | { 228 | var currentNode = FindNodeByPath(_rootNode, node.Path); 229 | if (currentNode != null) 230 | { 231 | currentNode.Name = textBoxDialogViewModel.Content; 232 | var newPath = Path.GetDirectoryName(node.Path) + "/" + textBoxDialogViewModel.Content; 233 | await _fileSystemService.RenameDirectoryAsync(node.Path, newPath); 234 | currentNode.Path = newPath; 235 | } 236 | } 237 | } 238 | private async void OpenDeleteDirectoryDialog(string path) 239 | { 240 | var options = new DialogOptions() 241 | { 242 | Title = "Are you sure you want to delete the selected directory?", 243 | Mode = DialogMode.None, 244 | Button = DialogButton.OKCancel, 245 | ShowInTaskBar = false, 246 | IsCloseButtonVisible = true, 247 | StartupLocation = WindowStartupLocation.CenterOwner, 248 | }; 249 | var baseDialogViewModel = new BaseDialogViewModel(); 250 | var result = await Dialog.ShowModal(baseDialogViewModel, options: options); 251 | if (result == DialogResult.OK) 252 | { 253 | var currentNode = FindNodeByPath(_rootNode, path); 254 | if (currentNode != null) 255 | { 256 | if (DeleteNodeByPath(_rootNode, path)) 257 | { 258 | await _fileSystemService.DeleteDirectoryAsync(path); 259 | } 260 | } 261 | } 262 | } 263 | private async void OpenCreateFileDialog(string path) 264 | { 265 | var options = new DialogOptions() 266 | { 267 | Title = "Input file name", 268 | Mode = DialogMode.None, 269 | Button = DialogButton.OKCancel, 270 | ShowInTaskBar = false, 271 | IsCloseButtonVisible = true, 272 | StartupLocation = WindowStartupLocation.CenterOwner, 273 | }; 274 | var textBoxDialogViewModel = new TextBoxDialogViewModel(); 275 | var result = await Dialog.ShowModal(textBoxDialogViewModel, options: options); 276 | if (result == DialogResult.OK) 277 | { 278 | var currentNode = FindNodeByPath(_rootNode, path); 279 | if (currentNode != null) 280 | { 281 | var file = new Node() 282 | { 283 | Name = textBoxDialogViewModel.Content, 284 | Path = path + "/" + textBoxDialogViewModel.Content, 285 | IsDirectory = false, 286 | }; 287 | InsertNode(currentNode, file); 288 | await _fileSystemService.CreateFileAsync(file.Path); 289 | } 290 | } 291 | } 292 | private async void OpenRenameFileDialog(Node node) 293 | { 294 | var options = new DialogOptions() 295 | { 296 | Title = "Input new file name", 297 | Mode = DialogMode.None, 298 | Button = DialogButton.OKCancel, 299 | ShowInTaskBar = false, 300 | IsCloseButtonVisible = true, 301 | StartupLocation = WindowStartupLocation.CenterOwner, 302 | }; 303 | var textBoxDialogViewModel = new TextBoxDialogViewModel() 304 | { 305 | Content = node.Name, 306 | }; 307 | var result = await Dialog.ShowModal(textBoxDialogViewModel, options: options); 308 | if (result == DialogResult.OK) 309 | { 310 | var currentNode = FindNodeByPath(_rootNode, node.Path); 311 | if (currentNode != null) 312 | { 313 | currentNode.Name = textBoxDialogViewModel.Content; 314 | var newPath = Path.GetDirectoryName(node.Path) + "/" + textBoxDialogViewModel.Content; 315 | await _fileSystemService.RenameDirectoryAsync(node.Path, newPath); 316 | currentNode.Path = newPath; 317 | } 318 | } 319 | } 320 | private async void OpenDeleteFileDialog(string path) 321 | { 322 | var options = new DialogOptions() 323 | { 324 | Title = "Are you sure you want to delete the selected file?", 325 | Mode = DialogMode.None, 326 | Button = DialogButton.OKCancel, 327 | ShowInTaskBar = false, 328 | IsCloseButtonVisible = true, 329 | StartupLocation = WindowStartupLocation.CenterOwner, 330 | }; 331 | var baseDialogViewModel = new BaseDialogViewModel(); 332 | var result = await Dialog.ShowModal(baseDialogViewModel, options: options); 333 | if (result == DialogResult.OK) 334 | { 335 | var currentNode = FindNodeByPath(_rootNode, path); 336 | if (currentNode != null) 337 | { 338 | if (DeleteNodeByPath(_rootNode, path)) 339 | { 340 | await _fileSystemService.DeleteFileAsync(path); 341 | } 342 | } 343 | } 344 | } 345 | } -------------------------------------------------------------------------------- /MatoEditor/ViewModels/EditorViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Windows.Input; 7 | using Avalonia.Controls; 8 | using Avalonia.Input; 9 | using AvaloniaEdit; 10 | using Markdown.Avalonia.Full; 11 | using MatoEditor.Dialogs; 12 | using MatoEditor.Models; 13 | using MatoEditor.Services; 14 | using ReactiveUI; 15 | using Ursa.Controls; 16 | 17 | namespace MatoEditor.ViewModels; 18 | 19 | public class EditorViewModel : ViewModelBase 20 | { 21 | public EditorViewModel(Window window, IFileSystemService fileSystemService, StorageService storageService) 22 | { 23 | _window = window; 24 | _fileSystemService = fileSystemService; 25 | _storageService = storageService; 26 | 27 | Editor = _window.FindControl("EditorUserControl").FindControl("TextEditor"); 28 | Editor.TextArea.TextView.LinkTextForegroundBrush = Editor.Foreground; 29 | Editor.KeyDown += Editor_KeyDown; 30 | 31 | Viewer = _window.FindControl("EditorUserControl").FindControl("MarkdownScrollViewer"); 32 | InsertSymbolCommand = ReactiveCommand.Create(InsertSymbol); 33 | 34 | ContentString = ""; 35 | FileTabs = new ObservableCollection(); 36 | DeleteFileTabCommand = ReactiveCommand.Create(DeleteFileTab); 37 | 38 | EditorVisible = true; 39 | ViewerVisible = true; 40 | EditorGridField = new GridField 41 | { 42 | Column = 0, 43 | ColumnSpan = 1 44 | }; 45 | ViewerGridField = new GridField 46 | { 47 | Column = 1, 48 | ColumnSpan = 1 49 | }; 50 | this.WhenAnyValue(x => x.ContentString) 51 | .Subscribe(ContentString => 52 | { 53 | try 54 | { 55 | Viewer.Markdown = ContentString; 56 | } 57 | catch (Exception e) 58 | { 59 | Viewer.Markdown = ""; 60 | Viewer.Markdown = ContentString; 61 | } 62 | if (SelectedFileTab != null) 63 | { 64 | this.SelectedFileTab.NewContentString = ContentString; 65 | } 66 | }); 67 | this.WhenAnyValue(x => x.SelectedFileTab.Path) 68 | .Subscribe(path => 69 | { 70 | if (path != null && path != "") 71 | { 72 | _storageService.CurrentFilePath = path; 73 | } 74 | }); 75 | _storageService.WhenAnyValue(x => x.CurrentFilePath) 76 | .Subscribe(async CurrentFilePath => 77 | { 78 | if (CurrentFilePath != null && CurrentFilePath != "") 79 | { 80 | var fileTab = FindFileTab(CurrentFilePath); 81 | if (fileTab == null) 82 | { 83 | var content = await GetContentStringFormFile(CurrentFilePath); 84 | var newFileTab = new FileTab() 85 | { 86 | Name = Path.GetFileName(CurrentFilePath), 87 | Path = CurrentFilePath, 88 | OldContentString = content, 89 | NewContentString = content 90 | }; 91 | FileTabs.Add(newFileTab); 92 | } 93 | SelectedFileTab = FindFileTab(CurrentFilePath); 94 | ContentString = SelectedFileTab.NewContentString; 95 | } 96 | }); 97 | } 98 | private readonly Window _window; 99 | private readonly IFileSystemService _fileSystemService; 100 | private StorageService _storageService; 101 | 102 | public TextEditor Editor { get; set; } 103 | public MarkdownScrollViewer Viewer { get; set; } 104 | 105 | private string _contentString; 106 | public string ContentString 107 | { 108 | get => _contentString; 109 | set => this.RaiseAndSetIfChanged(ref _contentString, value); 110 | } 111 | private ObservableCollection _fileTabs; 112 | public ObservableCollection FileTabs 113 | { 114 | get => _fileTabs; 115 | set => this.RaiseAndSetIfChanged(ref _fileTabs, value); 116 | } 117 | public ICommand DeleteFileTabCommand { get; } 118 | 119 | private FileTab _selectedFileTab; 120 | public FileTab SelectedFileTab 121 | { 122 | get => _selectedFileTab; 123 | set => this.RaiseAndSetIfChanged(ref _selectedFileTab, value); 124 | } 125 | public ICommand InsertSymbolCommand { get; } 126 | 127 | private bool _editorVisible; 128 | public bool EditorVisible 129 | { 130 | get => _editorVisible; 131 | set => this.RaiseAndSetIfChanged(ref _editorVisible, value); 132 | } 133 | private bool _viewerVisible; 134 | public bool ViewerVisible 135 | { 136 | get => _viewerVisible; 137 | set => this.RaiseAndSetIfChanged(ref _viewerVisible, value); 138 | } 139 | 140 | private GridField _editorGridField; 141 | public GridField EditorGridField 142 | { 143 | get => _editorGridField; 144 | set => this.RaiseAndSetIfChanged(ref _editorGridField, value); 145 | } 146 | private GridField _viewerGridField; 147 | public GridField ViewerGridField 148 | { 149 | get => _viewerGridField; 150 | set => this.RaiseAndSetIfChanged(ref _viewerGridField, value); 151 | } 152 | 153 | public class GridField : ReactiveObject 154 | { 155 | public GridField() 156 | { 157 | Row = 0; 158 | Column = 0; 159 | RowSpan = 0; 160 | ColumnSpan = 0; 161 | } 162 | 163 | private int _row; 164 | public int Row 165 | { 166 | get => _row; 167 | set => this.RaiseAndSetIfChanged(ref _row, value); 168 | } 169 | private int _column; 170 | public int Column 171 | { 172 | get => _column; 173 | set => this.RaiseAndSetIfChanged(ref _column, value); 174 | } 175 | private int _rowSpan; 176 | public int RowSpan 177 | { 178 | get => _rowSpan; 179 | set => this.RaiseAndSetIfChanged(ref _rowSpan, value); 180 | } 181 | private int _columnSpan; 182 | public int ColumnSpan 183 | { 184 | get => _columnSpan; 185 | set => this.RaiseAndSetIfChanged(ref _columnSpan, value); 186 | } 187 | } 188 | private async void InsertSymbol(string symbol) 189 | { 190 | var caretOffset = Editor.CaretOffset; 191 | if (symbol == "table") 192 | { 193 | var options = new DialogOptions() 194 | { 195 | Title = "Create Table", 196 | Mode = DialogMode.None, 197 | Button = DialogButton.OKCancel, 198 | ShowInTaskBar = false, 199 | IsCloseButtonVisible = true, 200 | StartupLocation = WindowStartupLocation.CenterOwner, 201 | }; 202 | var twoTextBoxDialogViewModel = new TwoNumberBoxDialogViewModel() 203 | { 204 | Label1 = "Row", 205 | Label2 = "Column", 206 | }; 207 | var result = await Dialog.ShowModal(twoTextBoxDialogViewModel, options: options); 208 | if (result == DialogResult.OK) 209 | { 210 | var row = twoTextBoxDialogViewModel.Number1; 211 | var column = twoTextBoxDialogViewModel.Number2; 212 | var sb = new System.Text.StringBuilder(); 213 | 214 | for (int i = 0; i < column; i++) 215 | { 216 | sb.Append("| "); 217 | } 218 | sb.AppendLine("|"); 219 | 220 | for (int i = 0; i < column; i++) 221 | { 222 | sb.Append("| --- "); 223 | } 224 | sb.AppendLine("|"); 225 | 226 | for (int i = 0; i < row; i++) 227 | { 228 | for (int j = 0; j < column; j++) 229 | { 230 | sb.Append("| "); 231 | } 232 | sb.AppendLine("|"); 233 | } 234 | 235 | symbol = sb.ToString(); 236 | } 237 | else 238 | { 239 | return; 240 | } 241 | } 242 | Editor.Document.Insert(caretOffset, symbol); 243 | Editor.CaretOffset = caretOffset + symbol.Length; 244 | } 245 | private async Task GetContentStringFormFile(string filePath) 246 | { 247 | return await _fileSystemService.ReadFileAsync(filePath); 248 | } 249 | public async Task SaveFile() 250 | { 251 | if (SelectedFileTab != null) 252 | { 253 | _ = await _fileSystemService.WriteFileAsync(SelectedFileTab.Path, SelectedFileTab.NewContentString); 254 | SelectedFileTab.OldContentString = SelectedFileTab.NewContentString; 255 | } 256 | } 257 | private void Editor_KeyDown(object sender, KeyEventArgs e) 258 | { 259 | if (e.Key == Key.S && e.KeyModifiers == KeyModifiers.Control) 260 | { 261 | SaveFile(); 262 | e.Handled = true; 263 | } 264 | } 265 | public void SetEditorMode(string mode) 266 | { 267 | if (mode == "edit") 268 | { 269 | EditorVisible = true; 270 | ViewerVisible = false; 271 | EditorGridField.Column = 0; 272 | EditorGridField.ColumnSpan = 2; 273 | ViewerGridField.Column = 1; 274 | ViewerGridField.ColumnSpan = 0; 275 | } 276 | else if (mode == "view") 277 | { 278 | EditorVisible = false; 279 | ViewerVisible = true; 280 | EditorGridField.Column = 1; 281 | EditorGridField.ColumnSpan = 0; 282 | ViewerGridField.Column = 0; 283 | ViewerGridField.ColumnSpan = 2; 284 | } 285 | else 286 | { 287 | EditorVisible = true; 288 | ViewerVisible = true; 289 | EditorGridField.Column = 0; 290 | EditorGridField.ColumnSpan = 1; 291 | ViewerGridField.Column = 1; 292 | ViewerGridField.ColumnSpan = 1; 293 | } 294 | } 295 | private FileTab? FindFileTab(string path) 296 | { 297 | return FileTabs.FirstOrDefault(fileTab => fileTab.Path == path); 298 | } 299 | private async void DeleteFileTab(string path) 300 | { 301 | var fileTab = FindFileTab(path); 302 | if (fileTab != null) 303 | { 304 | if (fileTab.OldContentString != fileTab.NewContentString) 305 | { 306 | var options = new DialogOptions() 307 | { 308 | Title = "Do you want to save the file changes?", 309 | Mode = DialogMode.None, 310 | Button = DialogButton.OKCancel, 311 | ShowInTaskBar = false, 312 | IsCloseButtonVisible = true, 313 | StartupLocation = WindowStartupLocation.CenterOwner, 314 | }; 315 | var baseDialogViewModel = new BaseDialogViewModel(); 316 | var result = await Dialog.ShowModal(baseDialogViewModel, options: options); 317 | if (result == DialogResult.OK) 318 | { 319 | var ok = await _fileSystemService.WriteFileAsync(fileTab.Path, fileTab.NewContentString); 320 | if (ok) 321 | { 322 | FileTabs.Remove(fileTab); 323 | } 324 | } 325 | } 326 | FileTabs.Remove(fileTab); 327 | } 328 | 329 | if (SelectedFileTab == null) 330 | { 331 | ContentString = ""; 332 | } 333 | } 334 | } -------------------------------------------------------------------------------- /MatoEditor/ViewModels/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using MatoEditor.Services; 3 | using MatoEditor.Views; 4 | 5 | namespace MatoEditor.ViewModels; 6 | 7 | public partial class MainWindowViewModel : ViewModelBase 8 | { 9 | private readonly Window _window; 10 | public NavigationViewModel NavigationViewModel { get; } 11 | public DocumentTreeViewModel DocumentTreeViewModel { get; } 12 | public EditorViewModel EditorViewModel { get; } 13 | 14 | public MainWindowViewModel(Window window, IFileSystemService fileSystemService, StorageService storageService, ConfigurationService configurationService) 15 | { 16 | _window = window; 17 | DocumentTreeViewModel = new DocumentTreeViewModel(fileSystemService, storageService); 18 | EditorViewModel = new EditorViewModel(window, fileSystemService, storageService); 19 | NavigationViewModel = new NavigationViewModel(window, storageService, configurationService, EditorViewModel); 20 | } 21 | } -------------------------------------------------------------------------------- /MatoEditor/ViewModels/NavigationViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows.Input; 4 | using Avalonia; 5 | using Avalonia.Controls; 6 | using Avalonia.Platform.Storage; 7 | using Avalonia.Styling; 8 | using MatoEditor.Services; 9 | using ReactiveUI; 10 | 11 | namespace MatoEditor.ViewModels; 12 | 13 | public class NavigationViewModel : ViewModelBase 14 | { 15 | public NavigationViewModel(Window window, StorageService storageService, ConfigurationService configurationService, EditorViewModel editorViewModel) 16 | { 17 | _window = window; 18 | _storageService = storageService; 19 | _configurationService = configurationService; 20 | _editorViewModel = editorViewModel; 21 | 22 | IsLight = Application.Current.RequestedThemeVariant == ThemeVariant.Light; 23 | 24 | SelectDirectoryCommand = ReactiveCommand.CreateFromTask(SelectDirectory); 25 | SaveFileCommand = ReactiveCommand.CreateFromTask(SaveFile); 26 | ChangeThemeCommand = ReactiveCommand.Create(ChangeTheme); 27 | SetEditorModeCommand = ReactiveCommand.Create(SetEditorMode); 28 | 29 | _storageService.WhenAnyValue(x => x.CurrentFilePath) 30 | .Subscribe(CurrentFilePath => 31 | { 32 | FilePath = CurrentFilePath; 33 | }); 34 | } 35 | private readonly Window _window; 36 | private StorageService _storageService; 37 | private ConfigurationService _configurationService; 38 | private EditorViewModel _editorViewModel; 39 | 40 | private string _filePath; 41 | public string FilePath 42 | { 43 | get => _filePath; 44 | set => this.RaiseAndSetIfChanged(ref _filePath, value); 45 | } 46 | 47 | private bool _isLight; 48 | public bool IsLight 49 | { 50 | get => _isLight; 51 | set => this.RaiseAndSetIfChanged(ref _isLight, value); 52 | } 53 | public ICommand SelectDirectoryCommand { get; } 54 | public ICommand SaveFileCommand { get; } 55 | public ICommand ChangeThemeCommand { get; } 56 | public ICommand SetEditorModeCommand { get; } 57 | 58 | private async Task SelectDirectory() 59 | { 60 | var directory = await _window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions 61 | { 62 | Title = "Select Directory" 63 | }); 64 | 65 | if (directory.Count > 0) 66 | { 67 | _storageService.RootDirectoryPath = directory[0].Path.LocalPath; 68 | _configurationService.SaveConfiguration(); 69 | } 70 | } 71 | private async Task SaveFile() 72 | { 73 | await _editorViewModel.SaveFile(); 74 | } 75 | private void ChangeTheme() 76 | { 77 | if (IsLight) 78 | { 79 | Application.Current.RequestedThemeVariant = ThemeVariant.Dark; 80 | _editorViewModel.Editor.TextArea.TextView.LinkTextForegroundBrush = _editorViewModel.Editor.Foreground; 81 | IsLight = false; 82 | } 83 | else 84 | { 85 | Application.Current.RequestedThemeVariant = ThemeVariant.Light; 86 | _editorViewModel.Editor.TextArea.TextView.LinkTextForegroundBrush = _editorViewModel.Editor.Foreground; 87 | IsLight = true; 88 | } 89 | _configurationService.SaveConfiguration(); 90 | } 91 | private void SetEditorMode(string mode) 92 | { 93 | _editorViewModel.SetEditorMode(mode); 94 | } 95 | } -------------------------------------------------------------------------------- /MatoEditor/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace MatoEditor.ViewModels; 4 | 5 | public class ViewModelBase : ReactiveObject 6 | { 7 | } -------------------------------------------------------------------------------- /MatoEditor/Views/DocumentTree.axaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /MatoEditor/Views/DocumentTree.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace MatoEditor.Views; 6 | 7 | public partial class DocumentTree : UserControl 8 | { 9 | public DocumentTree() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /MatoEditor/Views/Editor.axaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 88 | 95 | 102 | 109 | 116 | 123 | 130 | 137 | 144 | 151 | 158 | 165 | 172 | 179 | 186 | 193 | 194 | 202 | 203 | 204 | 205 | 206 | 207 | 212 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /MatoEditor/Views/Editor.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace MatoEditor.Views; 6 | 7 | public partial class Editor : UserControl 8 | { 9 | public Editor() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /MatoEditor/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /MatoEditor/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace MatoEditor.Views; 4 | 5 | public partial class MainWindow : Window 6 | { 7 | public MainWindow() 8 | { 9 | InitializeComponent(); 10 | } 11 | } -------------------------------------------------------------------------------- /MatoEditor/Views/Navigation.axaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 34 | 45 | 61 | 62 | 63 | 69 | 73 | 74 | 75 | 76 | 83 | 90 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /MatoEditor/Views/Navigation.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace MatoEditor.Views; 6 | 7 | public partial class Navigation : UserControl 8 | { 9 | public Navigation() 10 | { 11 | InitializeComponent(); 12 | } 13 | } -------------------------------------------------------------------------------- /MatoEditor/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

MatoEditor

3 |

An open-source markdown editor based on Avalonia

4 |
5 | 6 | ## Getting Started 7 | 8 | Download executable files from [Releases](https://github.com/CoolCoolTomato/MatoEditor/releases) 9 | 10 | ### Light 11 | 12 | ![Light](https://github.com/user-attachments/assets/dd297db8-7e83-4157-ba40-b2961c66439f) 13 | 14 | ### Dark 15 | 16 | ![Dark](https://github.com/user-attachments/assets/793919cf-1488-4ea5-8034-43b3f1adc719) 17 | 18 | ## Why MatoEditor 19 | 20 | - As a programmer, I think the best way to take notes is to use Markdown and organize them into folders. 21 | - Most note-taking software on the market has too many features, and many of them are unnecessary. 22 | 23 | ## Credits 24 | 25 | [Avalonia](https://github.com/AvaloniaUI/Avalonia) 26 | 27 | [Semi.Avalonia](https://github.com/irihitech/Semi.Avalonia) 28 | 29 | [Ursa.Avalonia](https://github.com/irihitech/Ursa.Avalonia) 30 | --------------------------------------------------------------------------------