Get way further

This commit is contained in:
2025-10-22 17:31:32 -05:00
parent 7c4f85ec21
commit 27f301fe48
39 changed files with 2028 additions and 123 deletions

View File

@@ -2,15 +2,21 @@
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;
@@ -26,6 +32,7 @@ public partial class MainWindowViewModel : ViewModelBase
public ObservableCollection<ProjectFileNode> ProjectFiles { get; } = new ObservableCollection<ProjectFileNode>();
public EventHandler<Tabalonia.Events.CloseLastTabEventArgs>? Closed { get; } = null;
public EventHandler<Tabalonia.Events.TabClosingEventArgs>? ClosingTab { get; }
public TabItemViewModel WelcomePage
{
@@ -49,16 +56,120 @@ public partial class MainWindowViewModel : ViewModelBase
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<GetWindowMessage>(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<GetWindowMessage>(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<GetWindowMessage>(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<GetWindowMessage>();
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);
this.TabItems.Add(WelcomePage);
}
@@ -74,7 +185,7 @@ public partial class MainWindowViewModel : ViewModelBase
var newTabItem = new NewTabViewModel(this, tab);
tab.Body = newTabItem;
}
return tab;
@@ -82,12 +193,19 @@ public partial class MainWindowViewModel : ViewModelBase
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]
@@ -102,7 +220,7 @@ public partial class MainWindowViewModel : ViewModelBase
private void Refresh(string obj)
{
for (int i = 0; i < TabItems.Count; i++)
@@ -122,11 +240,11 @@ public partial class MainWindowViewModel : ViewModelBase
OpenProjectConfig();
}),
new ProjectFileNode("Packages", ()=>{
new ProjectFileNode("Project Packages", ()=>{
OpenProjectPackages();
}),
new ProjectFileNode("Project Dependencies",()=>{
new ProjectFileNode("Project Dependencies (folders)",()=>{
OpenProjectDependencies();
})
])
@@ -155,7 +273,227 @@ public partial class MainWindowViewModel : ViewModelBase
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<RecoveryConfig>(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<GetWindowMessage>();
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))
@@ -185,7 +523,7 @@ public partial class MainWindowViewModel : ViewModelBase
private void OpenProjectConfig()
{
if (Directory.Exists(CurrentProject))
{
var config = Path.Combine(CurrentProject, "cross.json");
@@ -200,12 +538,23 @@ public partial class MainWindowViewModel : ViewModelBase
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 = new string[]{
".tcross",
@@ -232,12 +581,17 @@ public partial class MainWindowViewModel : ViewModelBase
public void OpenFile(string path)
{
bool isValid = false;
string ext = Path.GetExtension(path).ToLower();
foreach (var item in FILE_EXTS)
{
if (Path.GetExtension(path).ToLower() == item)
if (ext == item)
isValid = true;
}
if (!isValid) return;
if (!isValid)
{
OpenOtherFile(path, ext);
return;
}
foreach (var item in TabItems)
{
@@ -255,10 +609,39 @@ public partial class MainWindowViewModel : ViewModelBase
Header = Path.GetFileName(path),
};
tab.Body = new FileEditorViewModel(path,tab);
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)
@@ -337,13 +720,57 @@ public partial class MainWindowViewModel : ViewModelBase
SelectedTab?.Save();
}
[RelayCommand]
private void SaveAll()
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<GetWindowMessage>();
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