using System; using System.Collections.ObjectModel; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using CrossLangDevStudio.Messages; using CrossLangDevStudio.Models; using CrossLangDevStudio.Views; using MsBox.Avalonia; using MsBox.Avalonia.Enums; using Newtonsoft.Json; using Tabalonia.Events; namespace CrossLangDevStudio.ViewModels; public partial class MainWindowViewModel : ViewModelBase { public HttpClient Client { get; set; } = new(); public string CurrentProject { get; set; } = ""; public Func NewItemFactory => AddItem; public ObservableCollection TabItems { get; } = new(); public ObservableCollection ProjectFiles { get; } = new ObservableCollection(); public EventHandler? Closed { get; } = null; public EventHandler? ClosingTab { get; } public TabItemViewModel WelcomePage { get { return new TabItemViewModel() { Header = "Welcome", Body = new WelcomeViewModel(this) }; } } private ProjectFileNode? _selectedProjectFile = null; public ProjectFileNode? SelectedProjectFile { get => _selectedProjectFile; set { _selectedProjectFile = value; value?.Click?.Invoke(); } } private Avalonia.Threading.DispatcherTimer timer=new (); [RelayCommand] private void Welcome() { foreach (var item in TabItems) { if (item.Body is WelcomeViewModel) { SelectedTab = item; return; } } AddTab(WelcomePage); } [RelayCommand] private async Task WebsiteAsync() { var win = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); if (win is not null) { await win.Launcher.LaunchUriAsync(new Uri("https://crosslang.tesseslanguage.com/")); } } [RelayCommand] private async Task VideoDocumentationAsync() { var win = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); if (win is not null) { await win.Launcher.LaunchUriAsync(new Uri("https://peertube.site.tesses.net/c/crosslang")); } } [RelayCommand] private async Task DocumentationAsync() { var win = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); if (win is not null) { await win.Launcher.LaunchUriAsync(new Uri("https://crosslang.tesseslanguage.com/language-features/index.html")); } } private async Task CloseTabAsync(TabClosingEventArgs e, ISavable savable) { var box = MessageBoxManager .GetMessageBoxStandard("Save?", $"Do you want to save {Path.GetFileName(savable.FilePath)}.?", ButtonEnum.YesNoCancel, Icon.Question); var msg = await WeakReferenceMessenger.Default.Send(); if (msg is not null) { switch (await box.ShowWindowDialogAsync(msg)) { case ButtonResult.Yes: savable.Save(); break; case ButtonResult.Cancel: e.Cancel = true; break; } } } public bool HasUnsaved { get { foreach (var item in TabItems) { if (item.Body is ISavable s && s.Modified) return true; } return false; } } public MainWindowViewModel(string[] args) { this.ClosingTab = (sender, e) => { if (e.Item is TabItemViewModel vm) { if (vm.Body is ISavable savable) { if (savable.Modified) { //thanks https://github.com/AvaloniaUI/Avalonia/issues/4810#issuecomment-704372304 using (var source = new CancellationTokenSource()) { CloseTabAsync(e, savable).ContinueWith(e => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext()); Dispatcher.UIThread.MainLoop(source.Token); } } } } }; timer.Interval = TimeSpan.FromMinutes(1); timer.Tick += (sender, e) => { SaveRecovery(); }; if (args.Length == 1) { LoadProject(args[0]); } if (string.IsNullOrWhiteSpace(CurrentProject)) this.TabItems.Add(WelcomePage); } private object AddItem() { var tab = new TabItemViewModel { Header = "New Tab", }; if (Directory.Exists(CurrentProject)) { var newTabItem = new NewTabViewModel(this, tab); tab.Body = newTabItem; } return tab; } internal void LoadProject(string obj) { timer.Stop(); //we don't want autorecovery timer ticking when we change projects if (!File.Exists(Path.Combine(obj, "cross.json"))) { return; } CrossLangShell.EnsureRecent(obj); CurrentProject = obj.TrimEnd(Path.DirectorySeparatorChar); //we don't care that it isn't awaited due to only Refreshing here (you can referesh in GUI) //and the timer starting after async call inside the async function //so I think I know what I am doing #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed InitRecoveryAsync(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Refresh(CurrentProject); } [RelayCommand] public void RefreshListing() { if (!File.Exists(Path.Combine(CurrentProject, "cross.json"))) { return; } Refresh(CurrentProject); } private void Refresh(string obj) { for (int i = 0; i < TabItems.Count; i++) { if (TabItems[i].Body is WelcomeViewModel) { TabItems.RemoveAt(i); i--; } } ObservableCollection entries = new ObservableCollection { new ProjectFileNode("Project",[ new ProjectFileNode("Configuration",()=>{ OpenProjectConfig(); }), new ProjectFileNode("Project Packages", ()=>{ OpenProjectPackages(); }), new ProjectFileNode("Project Dependencies (folders)",()=>{ OpenProjectDependencies(); }) ]) }; void Itterate(ObservableCollection nodes, string dir) { foreach (var item in Directory.EnumerateDirectories(dir)) { ObservableCollection files = new(); Itterate(files, item); var pfn = new ProjectFileNode(Path.GetFileName(item), files); nodes.Add(pfn); } foreach (var item in Directory.EnumerateFiles(dir)) { nodes.Add(new ProjectFileNode(Path.GetFileName(item), () => { OpenFile(item); })); } } Itterate(entries, obj); ProjectFiles.Clear(); ProjectFiles.Add(new ProjectFileNode(Path.GetFileName(obj), entries)); } private int proj_id = -1; private async Task InitRecoveryAsync() { int i = 0; var dir = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "DevStudio", "Recovery"); foreach (var item in Directory.EnumerateDirectories(dir)) { if (int.TryParse(Path.GetFileName(item), out int id)) { if (id >= i) i = id + 1; var file = $"{item}.json"; if (File.Exists(file)) { var cfg = JsonConvert.DeserializeObject(File.ReadAllText(file)); if (cfg is not null) { if (cfg.ProjectDirectory == CurrentProject) { proj_id = id; if (cfg.HasRecovery) { var box = MessageBoxManager .GetMessageBoxStandard("Autorecovery", $"Found a autorecovery from {cfg.Date} do you want to load it?", ButtonEnum.YesNo, Icon.Question); var msg = await WeakReferenceMessenger.Default.Send(); if (msg is not null && await box.ShowWindowDialogAsync(msg) == ButtonResult.Yes) { CopyDir(item, CurrentProject); } cfg.HasRecovery = false; File.WriteAllText(file, JsonConvert.SerializeObject(cfg)); } //WeakReferenceMessenger.Default.Send timer.Start(); return; } } } } } proj_id = i; timer.Start(); } private void CopyDir(string src, string dest) { foreach (var dir in Directory.EnumerateDirectories(src)) { var destDir = Path.Combine(dest, Path.GetFileName(dir)); Directory.CreateDirectory(destDir); CopyDir(dir, destDir); } foreach (var file in Directory.EnumerateFiles(src)) { var destFile = Path.Combine(dest, Path.GetFileName(file)); File.Copy(file, destFile, true); } } private void SaveRecovery() { if (proj_id == -1 || !Directory.Exists(CurrentProject)) return; var id = proj_id; var dir = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "DevStudio", "Recovery", $"{id}_tmp"); var dirdest = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "DevStudio", "Recovery", $"{id}"); Directory.CreateDirectory(dir); bool hasAnything = false; foreach (var item in TabItems) { if (item.Body is ISavable savable && savable.Modified) { if (savable.FilePath.StartsWith(CurrentProject)) { var path = Path.GetRelativePath(CurrentProject, savable.FilePath); string? dirPath = Path.GetDirectoryName(path); if (dirPath is not null) Directory.CreateDirectory(Path.Combine(dir, dirPath)); savable.SaveRecovery(Path.Combine(dir, path)); hasAnything = true; } } } if (Directory.Exists(dirdest)) { Directory.Delete(dirdest, true); } if (hasAnything) { Directory.Move(dir, dirdest); File.WriteAllText($"{dirdest}.json", JsonConvert.SerializeObject(new RecoveryConfig { Date = DateTime.Now, ProjectDirectory = CurrentProject, HasRecovery=true })); } else { Directory.Delete(dir, true); File.WriteAllText($"{dirdest}.json", JsonConvert.SerializeObject(new RecoveryConfig { Date = DateTime.Now, ProjectDirectory = CurrentProject, HasRecovery=false })); } Directory.CreateDirectory(dirdest); } [RelayCommand] private void InstallTool() { foreach (var item in TabItems) { if (item.Body is PackageManagerViewModel vm) { if (vm.Packages is ToolPackageManager) { SelectedTab = item; return; } } } AddTab(new TabItemViewModel() { Header = "Tool Packages", Body = new PackageManagerViewModel(this, new ToolPackageManager()) }); } [RelayCommand] private void InstallTemplate() { foreach (var item in TabItems) { if (item.Body is PackageManagerViewModel vm) { if (vm.Packages is TemplatePackageManager) { SelectedTab = item; return; } } } AddTab(new TabItemViewModel() { Header = "Template Packages", Body = new PackageManagerViewModel(this, new TemplatePackageManager()) }); } [RelayCommand] private void InstallConsole() { foreach (var item in TabItems) { if (item.Body is PackageManagerViewModel vm) { if (vm.Packages is ConsolePackageManager) { SelectedTab = item; return; } } } AddTab(new TabItemViewModel() { Header = "Console Packages", Body = new PackageManagerViewModel(this, new ConsolePackageManager()) }); } [RelayCommand] private void InstallWebApp() { foreach (var item in TabItems) { if (item.Body is PackageManagerViewModel vm) { if (vm.Packages is WebAppPackageManager) { SelectedTab = item; return; } } } AddTab(new TabItemViewModel() { Header = "Web Application Packages", Body = new PackageManagerViewModel(this, new WebAppPackageManager()) }); } private void OpenProjectDependencies() { if (Directory.Exists(CurrentProject)) { var config = Path.Combine(CurrentProject, "cross.json"); if (File.Exists(config)) { foreach (var item in TabItems) { if (item.Body is AddProjectReferenceViewModel vm) { if (vm.ProjectDirectory == CurrentProject) { SelectedTab = item; return; } } } AddTab(new TabItemViewModel() { Header = "Project Dependencies (folders)", Body = new AddProjectReferenceViewModel(CurrentProject) }); } } } private void OpenProjectPackages() { if (Directory.Exists(CurrentProject)) { var config = Path.Combine(CurrentProject, "cross.json"); if (File.Exists(config)) { foreach (var item in TabItems) { if (item.Body is PackageManagerViewModel vm) { if (vm.Packages is ProjectPackageManager) { SelectedTab = item; return; } } } AddTab(new TabItemViewModel() { Header = "Project Packages", Body = new PackageManagerViewModel(this, new ProjectPackageManager(config)) }); } } } private void OpenProjectConfig() { if (Directory.Exists(CurrentProject)) { var config = Path.Combine(CurrentProject, "cross.json"); foreach (var item in TabItems) { if (item.Body is ProjectConfigurationViewModel model && model.FilePath == config) { SelectedTab = item; return; } } TabItemViewModel vm = new TabItemViewModel(); vm.Header = "Project Configuration"; var pcm = new ProjectConfigurationViewModel(config, vm); vm.Body = pcm; AddTab(vm); } } [RelayCommand] private async Task ReferenceAsync() { await CrossLangShell.RunCommandAsync(Environment.CurrentDirectory, "crosslang", "docs", "--reference"); var refFile = Path.Combine(CrossLangShell.CrossLangConfigDirectory, "Reference.crvm"); Console.WriteLine(refFile); if(File.Exists(refFile)) { OpenFile(refFile); } } static string[] FILE_EXTS = { ".tcross", ".json", ".txt", ".cpp", ".hpp", ".html", ".css", ".webmanifest", ".cs", ".c", ".h", ".xml", ".xaml", ".js", ".jsx", ".ts", ".tsx", ".gitignore", ".svg", ".md", ".yaml", ".yml", ".toml", ".tcasm", ".dart", ".bat", ".sh", ".lua", ".java", ".ini", ".go", ".crossarchiveignore" }; public void OpenFile(string path) { bool isValid = false; string ext = Path.GetExtension(path).ToLower(); foreach (var item in FILE_EXTS) { if (ext == item) isValid = true; } if (Path.GetFileNameWithoutExtension(path).ToLower() == "dockerfile") isValid = true; if (Path.GetFileNameWithoutExtension(path).ToLower() == "makefile") isValid = true; if (!isValid) { OpenOtherFile(path, ext); return; } foreach (var item in TabItems) { if (item.Body is FileEditorViewModel model) { if (model.FilePath == path) { SelectedTab = item; return; } } } var tab = new TabItemViewModel { Header = Path.GetFileName(path), }; tab.Body = new FileEditorViewModel(path, tab); AddTab(tab); } private void OpenOtherFile(string path, string ext) { if (ext == ".crvm") { foreach (var item in TabItems) { if (item.Body is CRVMViewerViewModel model) { if (model.FilePath == path) { SelectedTab = item; return; } } } try { var tab = new TabItemViewModel() { Header = Path.GetFileName(path), Body = new CRVMViewerViewModel(path) }; AddTab(tab); }catch(Exception ex) { Console.WriteLine(ex); } } } [ObservableProperty] private TabItemViewModel? _selectedTab = null; private void AddTab(TabItemViewModel item) { TabItems.Add(item); SelectedTab = item; } [RelayCommand] public async Task NewProjectAsync() { var res = await WeakReferenceMessenger.Default.Send(new NewProjectMessage()); if (!string.IsNullOrWhiteSpace(res)) { LoadProject(res); } } [RelayCommand] public async Task OpenProjectAsync() { var opts = new FolderPickerOpenOptions(); opts.AllowMultiple = false; opts.Title = "Open Project"; var window = await WeakReferenceMessenger.Default.Send(new GetWindowMessage()); opts.SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(CrossLangShell.Settings.ProjectDirectory) ?? null; var res = await window.StorageProvider.OpenFolderPickerAsync(opts); if (res.Count == 1) { var url = res[0].TryGetLocalPath(); if (!string.IsNullOrWhiteSpace(url)) { LoadProject(url); } } } [RelayCommand] private void OpenProjectInFileManager() { if (Directory.Exists(CurrentProject)) { CrossLangShell.OpenFolderInFileManager(CurrentProject); } } [RelayCommand] private void OpenProjectInTerminal() { if (Directory.Exists(CurrentProject)) { CrossLangShell.OpenTerminalInFolder(CurrentProject); } } [RelayCommand] private void BuildAndRun() { SaveAll(); if (Directory.Exists(CurrentProject)) { CrossLangShell.RunProjectInFolder(CurrentProject); } } [RelayCommand] private void Build() { SaveAll(); if (Directory.Exists(CurrentProject)) { CrossLangShell.BuildProjectInFolder(CurrentProject); } } [RelayCommand] private void Save() { SelectedTab?.Save(); } [RelayCommand] public void SaveAll() { foreach (var tab in TabItems) { tab.Save(); } } [RelayCommand] private void AddPackage() { if (Directory.Exists(CurrentProject)) { var conf = Path.Combine(CurrentProject, "cross.json"); if (File.Exists(conf)) { OpenProjectPackages(); } } } [RelayCommand] private async Task PublishAsync() { SaveAll(); if(Directory.Exists(CurrentProject)) await WeakReferenceMessenger.Default.Send(new PublishMessage(CurrentProject)); } [RelayCommand] private async Task PushPackageAsync() { SaveAll(); if (Directory.Exists(CurrentProject)) { var box = MessageBoxManager .GetMessageBoxStandard("Push Package Confirmation", $"Are you sure you want to push the package?", ButtonEnum.YesNo, Icon.Question); var msg = await WeakReferenceMessenger.Default.Send(); if (msg is not null && await box.ShowWindowDialogAsync(msg) == ButtonResult.Yes) CrossLangShell.PushProjectInFolder(CurrentProject); } } } public class RecoveryConfig { public DateTime Date { get; set; } public string ProjectDirectory { get; set; } = ""; public bool HasRecovery { get; set; } } public partial class NewTabViewModel : ViewModelBase { private MainWindowViewModel mainWindowViewModel; private TabItemViewModel tab; public NewTabViewModel(MainWindowViewModel mainWindowViewModel, TabItemViewModel tab) { this.mainWindowViewModel = mainWindowViewModel; this.tab = tab; } [ObservableProperty] private string _filePath = "src/"; [ObservableProperty] private decimal _progress = 0; [ObservableProperty] private string _url = ""; [RelayCommand] private void CreateDirectory() { if (!string.IsNullOrWhiteSpace(FilePath)) { Directory.CreateDirectory(Path.Combine(mainWindowViewModel.CurrentProject, FilePath)); mainWindowViewModel.RefreshListing(); mainWindowViewModel.TabItems.Remove(tab); } } [RelayCommand] private void CreateFile() { if (!string.IsNullOrWhiteSpace(FilePath)) { var path = Path.Combine(mainWindowViewModel.CurrentProject, FilePath); File.WriteAllText(path, ""); mainWindowViewModel.RefreshListing(); mainWindowViewModel.TabItems.Remove(tab); mainWindowViewModel.OpenFile(path); } } [RelayCommand] private async Task DownloadFileAsync() { try { using var destStrm = File.Create(Path.Combine(mainWindowViewModel.CurrentProject, FilePath)); var res = await mainWindowViewModel.Client.GetAsync(Url); long length = 0; long offset = 0; Progress = 0; if (res.IsSuccessStatusCode) { if (res.Content.Headers.ContentLength.HasValue) { length = res.Content.Headers.ContentLength.Value; byte[] array = new byte[1024]; int read = 0; using var src = await res.Content.ReadAsStreamAsync(); do { read = await src.ReadAsync(array); await destStrm.WriteAsync(array, 0, read); offset += read; if (length > 0) { Progress = (decimal)offset / (decimal)length; } } while (read != 0); } else { await res.Content.CopyToAsync(destStrm); } Progress = 1; mainWindowViewModel.RefreshListing(); mainWindowViewModel.TabItems.Remove(tab); } else { throw new Exception(); } } catch (Exception) { File.Delete(FilePath); this.Url = $"FAILED: {this.Url}"; } } }