├── img ├── dark_options.png ├── main_ubuntu.png └── main_windows.png ├── src ├── SmartCommander │ ├── Assets │ │ ├── file.png │ │ ├── image.png │ │ ├── main.ico │ │ ├── video.png │ │ ├── archive.png │ │ ├── document.png │ │ ├── folder.png │ │ ├── Resources.resx │ │ ├── Resources.ru-RU.resx │ │ ├── Resources.sr-Cyrl-RS.resx │ │ ├── Resources.sr-Latn-ME.resx │ │ └── Resources.fr-FR.resx │ ├── Plugins │ │ ├── TestBin │ │ │ └── CodeViewer.zip │ │ ├── PluginManager.cs │ │ └── ListerPluginWrapper.cs │ ├── Views │ │ ├── CopyMoveWindow.axaml.cs │ │ ├── OptionsWindow.axaml.cs │ │ ├── FileSearchWindow.axaml.cs │ │ ├── ProgressWindow.axaml │ │ ├── ViewerWindow.axaml │ │ ├── CopyMoveWindow.axaml │ │ ├── ProgressWindow.axaml.cs │ │ ├── FilesPane.axaml.cs │ │ ├── FileSearchWindow.axaml │ │ ├── MvvmMessageBoxEventArgs.cs │ │ ├── ViewerWindow.axaml.cs │ │ ├── OptionsWindow.axaml │ │ ├── MainWindow.axaml │ │ ├── MainWindow.axaml.cs │ │ └── FilesPane.axaml │ ├── SmartCancellationTokenSource.cs │ ├── ViewModels │ │ ├── ViewerViewModel.cs │ │ ├── CopyMoveViewModel.cs │ │ ├── ViewModelBase.cs │ │ ├── FileViewModel.cs │ │ ├── FileSearchViewModel.cs │ │ ├── OptionsViewModel.cs │ │ └── FilesPaneViewModel.cs │ ├── App.axaml │ ├── ViewLocator.cs │ ├── app.manifest │ ├── Converters │ │ └── BitmapValueConverter.cs │ ├── Extensions │ │ └── ObservableCollectionExtensions.cs │ ├── Behaviors │ │ └── DocumentTextBindingBehavior.cs │ ├── Models │ │ └── OptionsModel.cs │ ├── Program.cs │ ├── SmartCommander.csproj │ ├── App.axaml.cs │ └── Utils.cs └── SmartCommander.sln ├── .github └── workflows │ └── build.yml ├── README.md ├── installers └── SmartCommander_Windows.iss └── .gitignore /img/dark_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/img/dark_options.png -------------------------------------------------------------------------------- /img/main_ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/img/main_ubuntu.png -------------------------------------------------------------------------------- /img/main_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/img/main_windows.png -------------------------------------------------------------------------------- /src/SmartCommander/Assets/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Assets/file.png -------------------------------------------------------------------------------- /src/SmartCommander/Assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Assets/image.png -------------------------------------------------------------------------------- /src/SmartCommander/Assets/main.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Assets/main.ico -------------------------------------------------------------------------------- /src/SmartCommander/Assets/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Assets/video.png -------------------------------------------------------------------------------- /src/SmartCommander/Assets/archive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Assets/archive.png -------------------------------------------------------------------------------- /src/SmartCommander/Assets/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Assets/document.png -------------------------------------------------------------------------------- /src/SmartCommander/Assets/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Assets/folder.png -------------------------------------------------------------------------------- /src/SmartCommander/Plugins/TestBin/CodeViewer.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anovik/SmartCommander/HEAD/src/SmartCommander/Plugins/TestBin/CodeViewer.zip -------------------------------------------------------------------------------- /src/SmartCommander/Views/CopyMoveWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace SmartCommander.Views 4 | { 5 | public partial class CopyMoveWindow : Window 6 | { 7 | public CopyMoveWindow() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/OptionsWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace SmartCommander.Views 4 | { 5 | public partial class OptionsWindow : Window 6 | { 7 | public OptionsWindow() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/SmartCommander/SmartCancellationTokenSource.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace SmartCommander 4 | { 5 | class SmartCancellationTokenSource : CancellationTokenSource 6 | { 7 | public bool IsDisposed { get; private set; } 8 | protected override void Dispose(bool disposing) 9 | { 10 | base.Dispose(disposing); 11 | IsDisposed = true; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SmartCommander/Plugins/PluginManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmartCommander.Plugins; 4 | 5 | public static class PluginManager 6 | { 7 | public static ListerPluginWrapper CreateListerWrapper(string Filename) 8 | { 9 | return new ListerPluginWrapper(Filename); 10 | } 11 | 12 | public static IntPtr CreateListerWindow(this ListerPluginWrapper listerWrapper, IntPtr parentWindowHandle, string fileToLoad) 13 | { 14 | int showFlags = 1; 15 | return listerWrapper.LoadFile(parentWindowHandle, fileToLoad, showFlags); 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/SmartCommander/ViewModels/ViewerViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using System; 3 | using System.IO; 4 | 5 | namespace SmartCommander.ViewModels 6 | { 7 | public class ViewerViewModel : ViewModelBase 8 | { 9 | 10 | public ViewerViewModel(string filename) 11 | { 12 | try 13 | { 14 | Text = File.ReadAllText(filename); 15 | } 16 | catch { Text = ""; } 17 | Filename= filename; 18 | } 19 | 20 | public string Text { get; set; } 21 | public string Filename { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/SmartCommander/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ main ] 7 | 8 | env: 9 | DOTNET_VERSION: '8.0.x' 10 | 11 | jobs: 12 | build: 13 | 14 | name: build-${{matrix.os}} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Setup .NET Core SDK ${{ matrix.dotnet-version }} 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: ${{ env.DOTNET_VERSION }} 26 | 27 | - name: Install dependencies 28 | run: dotnet restore src 29 | - name: Build 30 | run: dotnet build src --configuration Release --no-restore 31 | -------------------------------------------------------------------------------- /src/SmartCommander/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using SmartCommander.Assets; 4 | using SmartCommander.ViewModels; 5 | using System; 6 | 7 | namespace SmartCommander 8 | { 9 | public class ViewLocator : IDataTemplate 10 | { 11 | public Control Build(object? data) 12 | { 13 | var name = data!.GetType().FullName!.Replace("ViewModel", "View"); 14 | var type = Type.GetType(name); 15 | 16 | if (type != null) 17 | { 18 | return (Control)Activator.CreateInstance(type)!; 19 | } 20 | else 21 | { 22 | return new TextBlock { Text = Resources.NotFound + name }; 23 | } 24 | } 25 | 26 | public bool Match(object? data) 27 | { 28 | return data is ViewModelBase; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/FileSearchWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Input; 3 | using Avalonia.Interactivity; 4 | 5 | namespace SmartCommander; 6 | 7 | public partial class FileSearchWindow : Window 8 | { 9 | public FileSearchWindow() 10 | { 11 | InitializeComponent(); 12 | 13 | var listBox = this.FindControl("SearchListBox"); 14 | listBox!.AddHandler(KeyDownEvent, (sender, e) => 15 | { 16 | if (e.Key == Key.Enter) 17 | { 18 | HandleItemAction((string)listBox.SelectedValue!); 19 | Close(); 20 | } 21 | }, RoutingStrategies.Tunnel); 22 | 23 | listBox!.DoubleTapped += (sender, e) => 24 | { 25 | HandleItemAction((string)listBox.SelectedValue!); 26 | Close(); 27 | }; 28 | } 29 | 30 | private void HandleItemAction(string? filename) 31 | { 32 | var viewModel=DataContext as FileSearchViewModel; 33 | viewModel!.ResultFilename = filename?? string.Empty; 34 | } 35 | } -------------------------------------------------------------------------------- /src/SmartCommander/Views/ProgressWindow.axaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/SmartCommander/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/SmartCommander.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32526.322 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartCommander", "SmartCommander\SmartCommander.csproj", "{613227E4-6B43-4185-8D02-1F0F3A3A3533}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {613227E4-6B43-4185-8D02-1F0F3A3A3533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {613227E4-6B43-4185-8D02-1F0F3A3A3533}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {613227E4-6B43-4185-8D02-1F0F3A3A3533}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {613227E4-6B43-4185-8D02-1F0F3A3A3533}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {16D87005-B764-4EE2-B78B-F5C14C636E65} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/ViewerWindow.axaml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/SmartCommander/ViewModels/CopyMoveViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using ReactiveUI; 3 | using SmartCommander.Assets; 4 | using System.Reactive; 5 | 6 | namespace SmartCommander.ViewModels 7 | { 8 | public class CopyMoveViewModel : ViewModelBase 9 | { 10 | public CopyMoveViewModel(bool copy, string text, string directory) 11 | { 12 | IsCopying = copy; 13 | Text = text; 14 | Directory = directory; 15 | 16 | OKCommand = ReactiveCommand.Create(SaveClose); 17 | CancelCommand = ReactiveCommand.Create(Close); 18 | } 19 | 20 | public bool IsCopying { get; set; } 21 | 22 | public string Text { get; set; } 23 | 24 | public string Directory { get; set; } 25 | 26 | public string CopyText => IsCopying ? string.Format(Resources.CopyTo, Text) : 27 | string.Format(Resources.MoveTo, Text); 28 | 29 | public ReactiveCommand OKCommand { get; } 30 | public ReactiveCommand CancelCommand { get; } 31 | 32 | public void SaveClose(Window window) 33 | { 34 | IsCopying = true; 35 | window?.Close(this); 36 | } 37 | 38 | public void Close(Window window) 39 | { 40 | IsCopying = false; 41 | window?.Close(this); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartCommander 2 | 3 | A two-panel cross-platform open-source file manager based on Avalonia (currently uses Avalonia version 11.0.10). Supports Windows and Linux. 4 | 5 | 6 | The main window on Ubuntu 22.04: 7 | 8 | 9 | SmartCommander 10 | 11 |

12 | The main window on Windows 11:

13 | 14 | ![alt SmartCommander](https://github.com/anovik/SmartCommander/blob/main/img/main_windows.png) 15 | 16 | ## How to build 17 | 18 | Please make sure you have .NET 8 SDK (or later) installed. 19 | ``` 20 | dotnet build src --configuration Release 21 | ``` 22 | ## Main features 23 | 24 | - Configurable interface (you can fully manage panels, columns, operations, themes, etc.) 25 | 26 | ![alt SmartCommanderOptions](https://github.com/anovik/SmartCommander/blob/main/img/dark_options.png) 27 | 28 | - All main operations with files/folders are supported(view, edit, copy, move, create new folder, delete, etc.) 29 | 30 | - Creating and unzipping of ZIP archives 31 | 32 | - Unicode support 33 | 34 | - Localization 35 | 36 | - Long operations are performed in the background 37 | 38 | - Search 39 | 40 | ## License 41 | 42 | SmartCommander is licensed under GNU GENERAL PUBLIC LICENSE v.3.0. 43 | 44 | ## Contribution 45 | 46 | Contributions are greatly appreciated. If you happen to have an issue with SmartCommander, please post it on GitHub using the Issues tab. 47 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/CopyMoveWindow.axaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/SmartCommander/Converters/BitmapValueConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | using Avalonia.Media.Imaging; 3 | using Avalonia.Platform; 4 | using System; 5 | using System.Globalization; 6 | using System.Reflection; 7 | 8 | namespace SmartCommander.Converters 9 | { 10 | public class BitmapValueConverter : IValueConverter 11 | { 12 | public static BitmapValueConverter Instance = new BitmapValueConverter(); 13 | 14 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 15 | { 16 | if (value == null) 17 | return null; 18 | 19 | if (value is string rawUri && targetType.IsAssignableFrom(typeof(Bitmap))) 20 | { 21 | Uri uri; 22 | 23 | // Allow for assembly overrides 24 | if (rawUri.StartsWith("avares://")) 25 | { 26 | uri = new Uri(rawUri); 27 | } 28 | else 29 | { 30 | var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name; 31 | uri = new Uri($"avares://{assemblyName}/{rawUri}"); 32 | } 33 | 34 | var asset = AssetLoader.Open(uri); 35 | 36 | return new Bitmap(asset); 37 | } 38 | 39 | throw new NotSupportedException(); 40 | } 41 | 42 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 43 | { 44 | throw new NotSupportedException(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/SmartCommander/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using MsBox.Avalonia.Enums; 2 | using ReactiveUI; 3 | using SmartCommander.Views; 4 | using System; 5 | 6 | namespace SmartCommander.ViewModels 7 | { 8 | public class ViewModelBase : ReactiveObject 9 | { 10 | public event EventHandler? MessageBoxRequest; 11 | public event EventHandler? MessageBoxInputRequest; 12 | public event EventHandler? ProgressRequest; 13 | 14 | protected void Progress_Show(int value) 15 | { 16 | if (this.ProgressRequest != null) 17 | { 18 | this.ProgressRequest(this, value); 19 | } 20 | } 21 | 22 | protected void MessageBox_Show(Action? resultAction, string messageBoxText, string caption = "", 23 | ButtonEnum button = ButtonEnum.Ok, Icon icon = Icon.None, object? parameter = null) 24 | { 25 | if (this.MessageBoxRequest != null) 26 | { 27 | this.MessageBoxRequest(this, new MvvmMessageBoxEventArgs(resultAction, null, messageBoxText, caption, 28 | button, icon, parameter)); 29 | } 30 | } 31 | 32 | protected void MessageBoxInput_Show(Action? resultAction, string messageBoxText, string caption = "") 33 | { 34 | if (this.MessageBoxInputRequest != null) 35 | { 36 | this.MessageBoxInputRequest(this, new MvvmMessageBoxEventArgs(null, resultAction, messageBoxText, 37 | caption, ButtonEnum.OkCancel)); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/SmartCommander/Extensions/ObservableCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Collections.Specialized; 4 | 5 | namespace SmartCommander.Extensions 6 | { 7 | public class BulkObservableCollection : ObservableCollection 8 | { 9 | private bool isNotifySuspended = false; 10 | public bool IsNotifySuspended => isNotifySuspended; 11 | 12 | private bool needsNotify = false; 13 | private int cachedCount = 0; 14 | 15 | protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 16 | { 17 | if (!isNotifySuspended) 18 | { 19 | base.OnCollectionChanged(e); 20 | } 21 | else 22 | { 23 | needsNotify = true; 24 | } 25 | } 26 | 27 | public void SuspendNotifies() 28 | { 29 | cachedCount = Items.Count; 30 | isNotifySuspended = true; 31 | needsNotify = false; 32 | } 33 | 34 | public void ResumeNotifies() 35 | { 36 | isNotifySuspended = false; 37 | if (needsNotify) 38 | { 39 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 40 | } 41 | } 42 | 43 | public void AddRange(IEnumerable newItems) 44 | { 45 | SuspendNotifies(); 46 | foreach (var item in newItems) 47 | { 48 | Add(item); 49 | } 50 | 51 | isNotifySuspended = false; 52 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, cachedCount)); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/SmartCommander/Behaviors/DocumentTextBindingBehavior.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Xaml.Interactivity; 3 | using AvaloniaEdit; 4 | using System; 5 | 6 | namespace SmartCommander.Behaviors 7 | { 8 | public class DocumentTextBindingBehavior : Behavior 9 | { 10 | private TextEditor? _textEditor; 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 | _textEditor.CaretOffset = caretOffset; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/ProgressWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Interactivity; 3 | using MsBox.Avalonia.Enums; 4 | using SmartCommander.ViewModels; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmartCommander.Views 9 | { 10 | public partial class ProgressWindow : Window 11 | { 12 | public MainWindowViewModel? ViewModel { get; set; } 13 | public ProgressWindow() 14 | { 15 | InitializeComponent(); 16 | Closing += ProgressWindow_Closing; 17 | cancelButton.Click += CancelButton_Click; 18 | } 19 | 20 | 21 | private async void CancelButton_Click(object? sender, RoutedEventArgs e) 22 | { 23 | await Cancel(); 24 | } 25 | 26 | private async void ProgressWindow_Closing(object? sender, WindowClosingEventArgs e) 27 | { 28 | if (!e.IsProgrammatic) 29 | { 30 | e.Cancel = true; 31 | await Cancel(); 32 | } 33 | } 34 | 35 | private async Task Cancel() 36 | { 37 | if (ViewModel != null && ViewModel.IsBackgroundOperation) 38 | { 39 | var messageBoxWindow = MsBox.Avalonia.MessageBoxManager 40 | .GetMessageBoxStandard(Assets.Resources.Alert, 41 | Assets.Resources.StopBackground + Environment.NewLine, 42 | ButtonEnum.YesNo, 43 | MsBox.Avalonia.Enums.Icon.Question); 44 | var result = await messageBoxWindow.ShowAsync(); 45 | if (result == ButtonResult.Yes) 46 | { 47 | ViewModel.Cancel(); 48 | Hide(); 49 | } 50 | } 51 | } 52 | 53 | public void SetProgress(int value) 54 | { 55 | if (value < 0) 56 | { 57 | value = 0; 58 | } 59 | if (value > 100) 60 | { 61 | value = 100; 62 | } 63 | progressBar.Value = value; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /installers/SmartCommander_Windows.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "SmartCommander" 5 | #define MyAppVersion "0.5" 6 | #define MyAppPublisher "Anna Novikova" 7 | #define MyAppURL "https://github.com/anovik/SmartCommander" 8 | #define MyAppExeName "SmartCommander.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{8B5B7F63-CC8C-46C3-89BD-7C57795F2D0A} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName={autopf}\{#MyAppName} 22 | DisableProgramGroupPage=yes 23 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 24 | ;PrivilegesRequired=lowest 25 | OutputBaseFilename=SmartCommander_setup 26 | SetupIconFile=..\src\SmartCommander\Assets\main.ico 27 | Compression=lzma 28 | SolidCompression=yes 29 | WizardStyle=modern 30 | 31 | [Languages] 32 | Name: "english"; MessagesFile: "compiler:Default.isl" 33 | Name: "french"; MessagesFile: "compiler:Languages\French.isl" 34 | Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" 35 | 36 | [Tasks] 37 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 38 | 39 | [Files] 40 | Source: "..\src\SmartCommander\bin\Release\net8.0\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion 41 | Source: "..\src\SmartCommander\bin\Release\net8.0\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 42 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 43 | 44 | [Icons] 45 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 46 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 47 | 48 | [Run] 49 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 50 | 51 | -------------------------------------------------------------------------------- /src/SmartCommander/Models/OptionsModel.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.IO; 5 | using static System.Environment; 6 | 7 | namespace SmartCommander.Models 8 | { 9 | public class OptionsModel 10 | { 11 | public static OptionsModel Instance { get; } = new OptionsModel(); 12 | static string _settingsDir = Path.Combine(GetFolderPath(SpecialFolder.ApplicationData), "SmartCommander"); 13 | static string _settingsPath = Path.Combine(_settingsDir, "settings.json"); 14 | static OptionsModel() 15 | { 16 | Directory.CreateDirectory(_settingsDir); 17 | if (File.Exists(_settingsPath)) 18 | { 19 | var options = JsonConvert.DeserializeObject(File.ReadAllText(_settingsPath)); 20 | if (options != null) 21 | { 22 | Instance = options; 23 | } 24 | } 25 | } 26 | public void Save() => File.WriteAllText(_settingsPath, JsonConvert.SerializeObject(this, Formatting.Indented)); 27 | 28 | 29 | public bool IsCurrentDirectoryDisplayed { get; set; } = true; 30 | 31 | public bool IsFunctionKeysDisplayed { get; set; } = true; 32 | 33 | public bool IsCommandLineDisplayed { get; set; } = true; 34 | 35 | public bool SaveWindowPositionSize { get; set; } = true; 36 | 37 | public bool IsHiddenSystemFilesDisplayed { get; set; } 38 | 39 | public bool SaveSettingsOnExit { get; set; } = true; 40 | 41 | public bool ConfirmationWhenDeleteNonEmpty { get; set; } = true; 42 | 43 | public double Top { get; set; } = -1; 44 | 45 | public double Left { get; set; } = -1; 46 | 47 | public double Width { get; set; } = -1; 48 | 49 | public double Height { get; set; } = -1; 50 | 51 | public bool IsMaximized { get; set; } 52 | 53 | public string LeftPanePath { get; set; } = ""; 54 | 55 | public string RightPanePath { get; set; } = ""; 56 | 57 | public bool IsDarkThemeEnabled { get; set; } 58 | public bool AllowOnlyOneInstance { get; set; } = true; 59 | public string Language { get; set; } = "en-US"; 60 | 61 | public List ListerPlugins { get; set; }= new List(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/SmartCommander/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | using Serilog; 4 | using SmartCommander.Models; 5 | using System; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.IO.Pipes; 9 | 10 | namespace SmartCommander 11 | { 12 | internal class Program 13 | { 14 | // Initialization code. Don't use any Avalonia, third-party APIs or any 15 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 16 | // yet and stuff might break. 17 | [STAThread] 18 | public static void Main(string[] args) 19 | { 20 | var haveSecondInstance = false; 21 | var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SmartCommander"); 22 | Directory.CreateDirectory(dir); 23 | 24 | string currentProcessName = Process.GetCurrentProcess().ProcessName; 25 | var runningProcesses = Process.GetProcessesByName(currentProcessName); 26 | haveSecondInstance = (runningProcesses.Length > 1); 27 | 28 | if (haveSecondInstance && OptionsModel.Instance.AllowOnlyOneInstance) 29 | { 30 | var client = new NamedPipeClientStream("SmartCommanderActivation"); 31 | client.Connect(1000); 32 | using (StreamWriter writer = new StreamWriter(client)) 33 | { 34 | writer.WriteLine("ActivateSmartCommander"); 35 | writer.Flush(); 36 | } 37 | } 38 | else { 39 | Log.Logger = new LoggerConfiguration() 40 | .MinimumLevel.Debug() 41 | #if DEBUG 42 | .WriteTo.Console() 43 | #endif 44 | .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) 45 | .CreateLogger(); 46 | 47 | BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); 48 | } 49 | 50 | } 51 | 52 | // Avalonia configuration, don't remove; also used by visual designer. 53 | public static AppBuilder BuildAvaloniaApp() 54 | => AppBuilder.Configure() 55 | .UsePlatformDetect() 56 | .LogToTrace() 57 | .UseReactiveUI(); 58 | 59 | 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/FilesPane.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Input; 4 | using Avalonia.Interactivity; 5 | using Avalonia.Markup.Xaml; 6 | using SmartCommander.ViewModels; 7 | using System; 8 | using System.IO; 9 | using System.Linq; 10 | 11 | namespace SmartCommander.Views 12 | { 13 | public partial class FilesPane : UserControl 14 | { 15 | private IFocusManager? focusManager { get; set; } 16 | static private Key[] gridhotkeys = [Key.Enter, Key.Back]; 17 | 18 | public FilesPane() 19 | { 20 | 21 | InitializeComponent(); 22 | 23 | if (OperatingSystem.IsWindows()) 24 | { 25 | var driveInfos = DriveInfo.GetDrives(); 26 | ComboBox? comboBox = this.Find("driveCombo"); 27 | if (comboBox != null) 28 | { 29 | comboBox.ItemsSource = driveInfos.Select(k => k.Name).ToList(); 30 | comboBox.SelectedIndex = 0; 31 | } 32 | } 33 | 34 | } 35 | 36 | private void OnLoaded(object sender, RoutedEventArgs e) 37 | { 38 | var PaneDataGrid = this.Get("PaneDataGrid"); 39 | if (PaneDataGrid != null) 40 | { 41 | focusManager = TopLevel.GetTopLevel((Visual)PaneDataGrid)?.FocusManager; 42 | PaneDataGrid.AddHandler(KeyDownEvent, dataGrid_PreviewKeyDown, RoutingStrategies.Tunnel); 43 | PaneDataGrid.ScrollIntoView(PaneDataGrid.SelectedItem, null); 44 | PaneDataGrid.Focus(); 45 | 46 | var viewModel = (FilesPaneViewModel?)DataContext; 47 | viewModel!.ScrollToItemRequested += (item, column) => 48 | { 49 | PaneDataGrid.ScrollIntoView(item, (DataGridColumn)column); 50 | PaneDataGrid.Focus(); 51 | }; 52 | } 53 | } 54 | 55 | 56 | 57 | public void dataGrid_PreviewKeyDown(object? sender, KeyEventArgs e) 58 | { 59 | if (gridhotkeys.Contains(e.Key) && ((focusManager?.GetFocusedElement() is DataGrid))) 60 | { 61 | var viewModel = DataContext as FilesPaneViewModel; 62 | if (e.Key == Key.Back) 63 | { 64 | viewModel?.ProcessCurrentItem(true); 65 | } 66 | 67 | if (e.Key == Key.Enter) 68 | { 69 | viewModel?.ProcessCurrentItem(); 70 | } 71 | } 72 | } 73 | 74 | 75 | private void InitializeComponent() 76 | { 77 | AvaloniaXamlLoader.Load(this); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/SmartCommander/Plugins/ListerPluginWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace SmartCommander.Plugins; 5 | 6 | public class ListerPluginWrapper : IDisposable 7 | { 8 | private IntPtr _pluginHandle; 9 | 10 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 11 | private delegate IntPtr ListLoadDelegate(IntPtr parentWin, string fileToLoad, int showFlags); 12 | 13 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 14 | private delegate void ListCloseWindowDelegate(IntPtr listWin); 15 | 16 | [UnmanagedFunctionPointer(CallingConvention.Cdecl)] 17 | private delegate void ListSendCommandDelegate(IntPtr listWin, int command, int parameter); 18 | 19 | [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 20 | public delegate void ListGetDetectStringDelegate(IntPtr detectString, int maxlen); 21 | 22 | private ListLoadDelegate ListLoad; 23 | private ListCloseWindowDelegate ListCloseWindow; 24 | private ListSendCommandDelegate ListSendCommand; 25 | private ListGetDetectStringDelegate ListGetDetectString; 26 | 27 | public ListerPluginWrapper(string pluginPath) 28 | { 29 | _pluginHandle = NativeLibrary.Load(pluginPath); 30 | 31 | ListLoad = Marshal.GetDelegateForFunctionPointer(NativeLibrary.GetExport(_pluginHandle, nameof(ListLoad))); 32 | ListCloseWindow = Marshal.GetDelegateForFunctionPointer(NativeLibrary.GetExport(_pluginHandle, nameof(ListCloseWindow))); 33 | ListSendCommand = Marshal.GetDelegateForFunctionPointer(NativeLibrary.GetExport(_pluginHandle, nameof(ListSendCommand))); 34 | ListGetDetectString = Marshal.GetDelegateForFunctionPointer(NativeLibrary.GetExport(_pluginHandle, nameof(ListGetDetectString))); 35 | } 36 | 37 | public IntPtr LoadFile(IntPtr parentWindowHandle, string filePath, int showFlags) 38 | { 39 | return ListLoad!(parentWindowHandle, filePath, showFlags); 40 | } 41 | 42 | public void CloseWindow(IntPtr listerWindowHandle) 43 | { 44 | ListCloseWindow!(listerWindowHandle); 45 | } 46 | 47 | public void SendCommand(IntPtr listerWindowHandle, int command, int parameter) 48 | { 49 | ListSendCommand!(listerWindowHandle, command, parameter); 50 | } 51 | 52 | public string? DetectString(int maxlen = 2000) 53 | { 54 | IntPtr detectStringPtr = Marshal.AllocHGlobal(maxlen); 55 | ListGetDetectString(detectStringPtr, maxlen); 56 | string? detectString = Marshal.PtrToStringAnsi(detectStringPtr); 57 | Marshal.FreeHGlobal(detectStringPtr); 58 | return detectString; 59 | } 60 | 61 | public void Dispose() 62 | { 63 | if (_pluginHandle != IntPtr.Zero) 64 | { 65 | NativeLibrary.Free(_pluginHandle); 66 | _pluginHandle = IntPtr.Zero; 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/FileSearchWindow.axaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 82 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/SmartCommander/ViewModels/FileViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using SmartCommander.Assets; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | 7 | namespace SmartCommander.ViewModels 8 | { 9 | public class FileViewModel : ViewModelBase 10 | { 11 | private string _name = ""; 12 | public static readonly List ImageExtensions = new List 13 | { "jpg", "jpeg", "jpe", "bmp", "tiff", "gif", "png" }; 14 | public static readonly List VideoExtensions = new List 15 | { "mp4", "mov", "avi", "wmv" }; 16 | public static readonly List ArchiveExtensions = new List 17 | { "zip", "rar", "7z" }; 18 | public static readonly List DocumentExtensions = new List 19 | { "doc", "docx", "txt","xslx", "xsl", "pdf" }; 20 | public FileViewModel() 21 | { 22 | 23 | } 24 | 25 | public FileViewModel(string fullName, bool isFolder) 26 | { 27 | FullName = fullName; 28 | IsFolder = isFolder; 29 | if (isFolder) 30 | { 31 | _name = Path.GetFileName(fullName); 32 | Extension = ""; 33 | Size = Resources.Folder; 34 | DateCreated = File.GetCreationTime(fullName); 35 | ImageSource = "Assets/folder.png"; 36 | } 37 | else 38 | { 39 | if (string.IsNullOrEmpty(Path.GetFileNameWithoutExtension(fullName))) 40 | { 41 | _name = Path.GetFileName(fullName); 42 | Extension = ""; 43 | } 44 | else 45 | { 46 | _name = Path.GetFileNameWithoutExtension(fullName); 47 | Extension = Path.GetExtension(fullName).TrimStart('.'); 48 | } 49 | Size = new FileInfo(fullName).Length.ToString(); 50 | DateCreated = File.GetCreationTime(fullName); 51 | if (ImageExtensions.Contains(Extension.ToLower())) 52 | { 53 | ImageSource = "Assets/image.png"; 54 | } 55 | else if (VideoExtensions.Contains(Extension.ToLower())) 56 | { 57 | ImageSource = "Assets/video.png"; 58 | } 59 | else if (ArchiveExtensions.Contains(Extension.ToLower())) 60 | { 61 | ImageSource = "Assets/archive.png"; 62 | } 63 | else if (DocumentExtensions.Contains(Extension.ToLower())) 64 | { 65 | ImageSource = "Assets/document.png"; 66 | } 67 | else 68 | { 69 | ImageSource = "Assets/file.png"; 70 | } 71 | } 72 | } 73 | public string FullName { get; set; } = ""; 74 | public string Name 75 | { 76 | get 77 | { 78 | return _name; 79 | } 80 | set 81 | { 82 | if (string.IsNullOrEmpty(value) || value == _name) 83 | { 84 | return; 85 | } 86 | 87 | string destination = ""; 88 | 89 | // moving here is fast since they are guaranteed to be on the same drive 90 | if (IsFolder) 91 | { 92 | destination = Path.Combine(Path.GetDirectoryName(FullName) ?? "", value); 93 | Directory.Move(FullName, destination); 94 | } 95 | else 96 | { 97 | var destName = value; 98 | if (!string.IsNullOrEmpty(Extension)) 99 | { 100 | destName += "." + Extension; 101 | } 102 | destination = Path.Combine(Path.GetDirectoryName(FullName) ?? "", destName); 103 | File.Move(FullName, destination); 104 | } 105 | _name = value; 106 | FullName = destination; 107 | this.RaisePropertyChanged(nameof(Name)); 108 | this.RaisePropertyChanged(nameof(FullName)); 109 | } 110 | } 111 | 112 | public string Extension { get; set; } = ""; 113 | public string Size { get; set; } = ""; 114 | public DateTime DateCreated { get; set; } 115 | public bool IsFolder { get; set; } 116 | 117 | public string? ImageSource { get; set; } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 96 | 98 | 100 | 102 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/SmartCommander/ViewModels/FileSearchViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Threading; 2 | using ReactiveUI; 3 | using Serilog; 4 | using SmartCommander.Extensions; 5 | using SmartCommander.ViewModels; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Reactive; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | public class FileSearchViewModel : ViewModelBase 14 | { 15 | private string _currentFolder = ""; 16 | private string _statusFolder = ""; 17 | private string _fileMask = ""; 18 | private bool _isSearching = false; 19 | private CancellationTokenSource? _cancellationTokenSource; 20 | private Timer? _timer; 21 | 22 | public string CurrentFolder 23 | { 24 | get => _currentFolder; 25 | set => this.RaiseAndSetIfChanged(ref _currentFolder, value); 26 | } 27 | 28 | public string StatusFolder { get; set; } = ""; 29 | 30 | public bool TopDirectoryOnly { get; set; } 31 | 32 | public bool SearchContent { get; set; } 33 | 34 | public string SearchText { get; set; } = ""; 35 | 36 | public string FileMask 37 | { 38 | get => _fileMask; 39 | set => this.RaiseAndSetIfChanged(ref _fileMask, value); 40 | } 41 | 42 | public bool IsSearching 43 | { 44 | get => _isSearching; 45 | set => this.RaiseAndSetIfChanged(ref _isSearching, value); 46 | } 47 | 48 | public string ResultFilename { get; set; } = string.Empty; 49 | 50 | public BulkObservableCollection SearchResults { get; } 51 | public ReactiveCommand StartSearchCommand { get; } 52 | public ReactiveCommand CancelSearchCommand { get; } 53 | 54 | public FileSearchViewModel(string folder = "") 55 | { 56 | CurrentFolder = folder ?? "c:\\"; 57 | FileMask = "*.txt"; 58 | SearchResults = new BulkObservableCollection(); 59 | StartSearchCommand = ReactiveCommand.CreateFromTask(StartSearch); 60 | CancelSearchCommand = ReactiveCommand.Create(CancelSearch); 61 | } 62 | 63 | public async Task SearchAsync(string folderPath, string searchPattern, CancellationToken cancellationToken) 64 | { 65 | try 66 | { 67 | cancellationToken.ThrowIfCancellationRequested(); 68 | 69 | _statusFolder = folderPath; 70 | var findedFolderAndFiles = new List(); 71 | 72 | if (SearchContent) 73 | { 74 | var files = Directory.GetFiles(folderPath, "*", SearchOption.TopDirectoryOnly); 75 | foreach(var file in files) 76 | { 77 | cancellationToken.ThrowIfCancellationRequested(); 78 | foreach (string line in File.ReadLines(file)) 79 | { 80 | if (line.Contains(searchPattern)) 81 | { 82 | SearchResults.Add(file); 83 | break; 84 | } 85 | } 86 | } 87 | 88 | } 89 | else 90 | { 91 | var dirs = Directory.GetDirectories(folderPath, searchPattern, SearchOption.TopDirectoryOnly); 92 | findedFolderAndFiles.AddRange(dirs); 93 | var files = Directory.GetFiles(folderPath, searchPattern); 94 | findedFolderAndFiles.AddRange(files); 95 | SearchResults.AddRange(findedFolderAndFiles); 96 | } 97 | 98 | if (!TopDirectoryOnly) 99 | { 100 | var subDirectories = Directory.GetDirectories(folderPath); 101 | foreach (var subDir in subDirectories) 102 | { 103 | cancellationToken.ThrowIfCancellationRequested(); 104 | await SearchAsync(subDir, searchPattern, cancellationToken); 105 | } 106 | } 107 | } 108 | catch (OperationCanceledException e) 109 | { 110 | Log.Error("OperationCanceledException: " + e.Message); 111 | } 112 | catch (UnauthorizedAccessException e) 113 | { 114 | Log.Error("UnauthorizedAccessException: " + e.Message); 115 | } 116 | catch (Exception e) 117 | { 118 | Log.Error("Exception: " + e.Message); 119 | } 120 | 121 | return true; 122 | } 123 | 124 | private async Task StartSearch() 125 | { 126 | if (string.IsNullOrEmpty(CurrentFolder) || string.IsNullOrEmpty(FileMask)) 127 | return; 128 | 129 | IsSearching = true; 130 | SearchResults.Clear(); 131 | _cancellationTokenSource = new CancellationTokenSource(); 132 | _timer = new Timer(OnTimerTick, null, 0, 500); 133 | await Task.Run(() => SearchAsync(CurrentFolder, SearchContent ? SearchText : FileMask, _cancellationTokenSource.Token)); 134 | 135 | _statusFolder = ""; 136 | IsSearching = false; 137 | } 138 | 139 | private void OnTimerTick(object? state) 140 | { 141 | Dispatcher.UIThread.Post(() => 142 | { 143 | StatusFolder = _statusFolder; 144 | this.RaisePropertyChanged(nameof(StatusFolder)); 145 | }); 146 | } 147 | 148 | public void CancelSearch() 149 | { 150 | _cancellationTokenSource?.Cancel(); 151 | IsSearching = false; 152 | _timer?.Dispose(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/SmartCommander/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.ApplicationLifetimes; 4 | using Avalonia.Markup.Xaml; 5 | using ReactiveUI; 6 | using SmartCommander.Models; 7 | using SmartCommander.ViewModels; 8 | using SmartCommander.Views; 9 | using System.IO.Pipes; 10 | using System.IO; 11 | using System.Threading.Tasks; 12 | using Avalonia.Threading; 13 | 14 | namespace SmartCommander 15 | { 16 | public partial class App : Application 17 | { 18 | private MainWindow? mainWindow; 19 | private TrayIcon? trayIcon; 20 | private MainWindowViewModel? vm; 21 | 22 | public override void Initialize() 23 | { 24 | AvaloniaXamlLoader.Load(this); 25 | } 26 | 27 | public override void OnFrameworkInitializationCompleted() 28 | { 29 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 30 | { 31 | vm = new MainWindowViewModel(); 32 | mainWindow = new MainWindow 33 | { 34 | DataContext = vm, 35 | }; 36 | 37 | mainWindow.PropertyChanged += MainWindow_PropertyChanged; 38 | 39 | desktop.MainWindow = mainWindow; 40 | 41 | RegisterTrayIcon(); 42 | 43 | ((IClassicDesktopStyleApplicationLifetime)ApplicationLifetime).ShutdownRequested += App_ShutdownRequested; 44 | } 45 | 46 | StartServer(); 47 | 48 | base.OnFrameworkInitializationCompleted(); 49 | } 50 | 51 | private void MainWindow_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) 52 | { 53 | if (sender is MainWindow && e.NewValue is WindowState windowState) 54 | { 55 | if (windowState == WindowState.Minimized) 56 | { 57 | mainWindow?.Hide(); 58 | if (trayIcon != null) 59 | { 60 | trayIcon.IsVisible = true; 61 | } 62 | } 63 | else 64 | { 65 | mainWindow?.Show(); 66 | if (trayIcon != null) 67 | { 68 | trayIcon.IsVisible = false; 69 | } 70 | } 71 | } 72 | } 73 | 74 | private void RegisterTrayIcon() 75 | { 76 | trayIcon = new TrayIcon 77 | { 78 | ToolTipText = "Smart Commander", 79 | Command = ReactiveCommand.Create(ShowApplication), 80 | Icon = mainWindow?.Icon, 81 | Menu = new NativeMenu() 82 | }; 83 | 84 | trayIcon.Menu.Add(new NativeMenuItem("Exit") { Command = vm?.ExitCommand }); 85 | 86 | var trayIcons = new TrayIcons 87 | { 88 | trayIcon 89 | }; 90 | 91 | SetValue(TrayIcon.IconsProperty, trayIcons); 92 | 93 | trayIcon.IsVisible = false; 94 | } 95 | 96 | private void ShowApplication() 97 | { 98 | if (mainWindow != null) 99 | { 100 | mainWindow.WindowState = WindowState.Maximized; 101 | mainWindow.Topmost = true; 102 | mainWindow.Show(); 103 | mainWindow.Topmost = false; 104 | } 105 | } 106 | 107 | private void App_ShutdownRequested(object? sender, ShutdownRequestedEventArgs e) 108 | { 109 | var desktop = sender as ClassicDesktopStyleApplicationLifetime; 110 | if (desktop != null && desktop.MainWindow != null) 111 | { 112 | MainWindowViewModel? viewModel = desktop.MainWindow.DataContext as MainWindowViewModel; 113 | if (viewModel != null) 114 | { 115 | OptionsModel.Instance.LeftPanePath = viewModel.LeftFileViewModel.CurrentDirectory; 116 | OptionsModel.Instance.RightPanePath = viewModel.RightFileViewModel.CurrentDirectory; 117 | } 118 | } 119 | if (OptionsModel.Instance.SaveWindowPositionSize) 120 | { 121 | if (desktop != null && desktop.MainWindow != null) 122 | { 123 | OptionsModel.Instance.Left = desktop.MainWindow.Bounds.Left; 124 | OptionsModel.Instance.Width = desktop.MainWindow.Bounds.Width; 125 | OptionsModel.Instance.Top = desktop.MainWindow.Bounds.Top; 126 | OptionsModel.Instance.Height = desktop.MainWindow.Bounds.Height; 127 | OptionsModel.Instance.IsMaximized = desktop.MainWindow.WindowState == WindowState.Maximized; 128 | } 129 | } 130 | 131 | if (OptionsModel.Instance.SaveSettingsOnExit) 132 | { 133 | OptionsModel.Instance.Save(); 134 | } 135 | } 136 | 137 | void StartServer() 138 | { 139 | Task.Run(() => 140 | { 141 | while (true) 142 | { 143 | var server = new NamedPipeServerStream("SmartCommanderActivation"); 144 | server.WaitForConnection(); 145 | using (StreamReader reader = new StreamReader(server)) 146 | { 147 | var line = reader.ReadLine(); 148 | if (line == "ActivateSmartCommander") 149 | { 150 | Dispatcher.UIThread.Post(() => ShowApplication()); 151 | } 152 | } 153 | } 154 | }); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.ReactiveUI; 3 | using MsBox.Avalonia.Enums; 4 | using ReactiveUI; 5 | using SmartCommander.Models; 6 | using SmartCommander.ViewModels; 7 | using System; 8 | using System.Threading.Tasks; 9 | 10 | namespace SmartCommander.Views 11 | { 12 | public partial class MainWindow : ReactiveWindow 13 | { 14 | ProgressWindow progressWindow; 15 | public MainWindow() 16 | { 17 | Opened += OnOpened; 18 | InitializeComponent(); 19 | 20 | this.WhenActivated(d => d(ViewModel!.ShowCopyDialog.RegisterHandler( 21 | interaction => DoShowDialogAsync(interaction) 22 | ))); 23 | this.WhenActivated(d => d(ViewModel!.ShowOptionsDialog.RegisterHandler( 24 | interaction => DoShowDialogAsync(interaction) 25 | ))); 26 | this.WhenActivated(d => d(ViewModel!.LeftFileViewModel.ShowViewerDialog.RegisterHandler( 27 | interaction => DoShowDialogAsync(interaction) 28 | ))); 29 | this.WhenActivated(d => d(ViewModel!.RightFileViewModel.ShowViewerDialog.RegisterHandler( 30 | interaction => DoShowDialogAsync(interaction) 31 | ))); 32 | this.WhenActivated(d => d(ViewModel!.ShowSearchsDialog.RegisterHandler( 33 | interaction => DoShowDialogAsync(interaction) 34 | ))); 35 | 36 | progressWindow = new ProgressWindow(); 37 | 38 | Closing += async (s, e) => 39 | { 40 | if (!e.IsProgrammatic) 41 | { 42 | MainWindowViewModel? vm = DataContext as MainWindowViewModel; 43 | if (vm != null) 44 | { 45 | if (vm.IsBackgroundOperation) 46 | { 47 | e.Cancel = true; 48 | var messageBoxWindow = MsBox.Avalonia.MessageBoxManager 49 | .GetMessageBoxStandard(Assets.Resources.Alert, 50 | Assets.Resources.StopBackground + Environment.NewLine, 51 | ButtonEnum.YesNo, 52 | MsBox.Avalonia.Enums.Icon.Question); 53 | var result = await messageBoxWindow.ShowAsPopupAsync(this); 54 | if (result == ButtonResult.Yes) 55 | { 56 | vm.Cancel(); 57 | this.Close(); 58 | } 59 | else 60 | { 61 | return; 62 | } 63 | } 64 | } 65 | 66 | progressWindow.Close(); 67 | } 68 | }; 69 | } 70 | 71 | private async Task DoShowDialogAsync(IInteractionContext interaction) 72 | where T2 : Window, new() 73 | { 74 | var dialog = new T2(); 75 | dialog.DataContext = interaction.Input; 76 | dialog.Activate(); 77 | 78 | var result = await dialog.ShowDialog(this); 79 | interaction.SetOutput(result); 80 | } 81 | 82 | private void OnOpened(object? sender, EventArgs e) 83 | { 84 | if (OptionsModel.Instance.SaveWindowPositionSize && 85 | OptionsModel.Instance.Left > -1 && 86 | OptionsModel.Instance.Width > -1 && 87 | OptionsModel.Instance.Top > -1 && 88 | OptionsModel.Instance.Height > -1) 89 | { 90 | if (OptionsModel.Instance.IsMaximized) 91 | { 92 | WindowState = WindowState.Maximized; 93 | } 94 | else 95 | { 96 | WindowState = WindowState.Normal; 97 | this.Arrange(new Avalonia.Rect(OptionsModel.Instance.Left, OptionsModel.Instance.Top, OptionsModel.Instance.Width, OptionsModel.Instance.Height)); 98 | } 99 | } 100 | 101 | MainWindowViewModel? vm = DataContext as MainWindowViewModel; 102 | 103 | if (vm != null) 104 | { 105 | progressWindow.ViewModel = vm; 106 | LeftPane.DataContext = vm.LeftFileViewModel; 107 | RightPane.DataContext = vm.RightFileViewModel; 108 | 109 | vm.MessageBoxRequest += View_MessageBoxRequest; 110 | vm.LeftFileViewModel.MessageBoxRequest += View_MessageBoxRequest; 111 | vm.RightFileViewModel.MessageBoxRequest += View_MessageBoxRequest; 112 | 113 | vm.MessageBoxInputRequest += View_MessageBoxInputRequest; 114 | vm.LeftFileViewModel.MessageBoxInputRequest += View_MessageBoxInputRequest; 115 | vm.RightFileViewModel.MessageBoxInputRequest += View_MessageBoxInputRequest; 116 | 117 | vm.ProgressRequest += View_ProgressRequest; 118 | } 119 | } 120 | 121 | private void View_ProgressRequest(object? sender, int e) 122 | { 123 | if (e == 0) 124 | { 125 | progressWindow.Show(); 126 | } 127 | if (e >= 100) 128 | { 129 | progressWindow.Hide(); 130 | } 131 | progressWindow.SetProgress(e); 132 | } 133 | 134 | void View_MessageBoxRequest(object? sender, MvvmMessageBoxEventArgs e) 135 | { 136 | e.Show(this); 137 | } 138 | 139 | void View_MessageBoxInputRequest(object? sender, MvvmMessageBoxEventArgs e) 140 | { 141 | e.ShowInput(this); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/SmartCommander/Views/FilesPane.axaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 50 | 63 | 64 | 65 | 68 | 71 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 97 | 98 | 99 | 100 | 101 | 104 | 107 | 110 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/SmartCommander/ViewModels/OptionsViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.ApplicationLifetimes; 4 | using Avalonia.Platform.Storage; 5 | using AvaloniaEdit.Utils; 6 | using ReactiveUI; 7 | using SmartCommander.Assets; 8 | using SmartCommander.Models; 9 | using System.Collections.Generic; 10 | using System.Collections.ObjectModel; 11 | using System.Globalization; 12 | using System.Linq; 13 | using System.Reactive; 14 | using System.Resources; 15 | 16 | namespace SmartCommander.ViewModels 17 | { 18 | public class OptionsViewModel : ViewModelBase 19 | { 20 | 21 | public ObservableCollection AvailableCultures { get; } 22 | 23 | public CultureInfo SelectedCulture { get; set; } 24 | 25 | 26 | public ObservableCollection ListerPlugins { get; set; } = new(); 27 | private string _selectedPlugin = string.Empty; 28 | public string SelectedPlugin 29 | { 30 | get => _selectedPlugin; 31 | set => this.RaiseAndSetIfChanged(ref _selectedPlugin, value); 32 | } 33 | 34 | private static IEnumerable GetAvailableCultures() 35 | { 36 | List result = new List(); 37 | ResourceManager rm = new ResourceManager(typeof(Resources)); 38 | CultureInfo[] cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); 39 | 40 | foreach (CultureInfo culture in cultures) 41 | { 42 | if (culture.Equals(CultureInfo.InvariantCulture)) 43 | { 44 | result.Add(new CultureInfo("en-US")); 45 | continue; 46 | } 47 | 48 | ResourceSet? rs = rm?.GetResourceSet(culture, true, false); 49 | if (rs != null) 50 | result.Add(culture); 51 | } 52 | return result; 53 | } 54 | 55 | public OptionsViewModel() 56 | { 57 | OKCommand = ReactiveCommand.Create(SaveClose); 58 | CancelCommand = ReactiveCommand.Create(Close); 59 | 60 | IsCurrentDirectoryDisplayed = Model.IsCurrentDirectoryDisplayed; 61 | IsFunctionKeysDisplayed = Model.IsFunctionKeysDisplayed; 62 | IsCommandLineDisplayed = Model.IsCommandLineDisplayed; 63 | IsHiddenSystemFilesDisplayed = Model.IsHiddenSystemFilesDisplayed; 64 | SaveSettingsOnExit = Model.SaveSettingsOnExit; 65 | ConfirmationWhenDeleteNonEmpty = Model.ConfirmationWhenDeleteNonEmpty; 66 | SaveWindowPositionSize = Model.SaveWindowPositionSize; 67 | IsDarkThemeEnabled = Model.IsDarkThemeEnabled; 68 | AllowOnlyOneInstance = Model.AllowOnlyOneInstance; 69 | 70 | AvailableCultures = new ObservableCollection(GetAvailableCultures()); 71 | var lang = AvailableCultures.First(x => x.Name == Model.Language); 72 | SelectedCulture = lang ?? AvailableCultures.First(); 73 | 74 | ListerPlugins.AddRange(Model.ListerPlugins); 75 | AddFileCommand = ReactiveCommand.Create(AddFileAsync); 76 | RemoveFileCommand = ReactiveCommand.Create(RemoveFile); 77 | } 78 | 79 | public bool IsCurrentDirectoryDisplayed { get; set; } 80 | 81 | public bool IsFunctionKeysDisplayed { get; set; } 82 | 83 | public bool IsCommandLineDisplayed { get; set; } 84 | 85 | public bool IsHiddenSystemFilesDisplayed { get; set; } 86 | 87 | public bool SaveSettingsOnExit { get; set; } 88 | 89 | public bool ConfirmationWhenDeleteNonEmpty { get; set; } 90 | 91 | public bool SaveWindowPositionSize { get; set; } 92 | 93 | public bool IsDarkThemeEnabled { get; set; } 94 | public bool AllowOnlyOneInstance { get; set; } 95 | 96 | public ReactiveCommand OKCommand { get; } 97 | public ReactiveCommand CancelCommand { get; } 98 | public ReactiveCommand AddFileCommand { get; } 99 | public ReactiveCommand RemoveFileCommand { get; } 100 | private void RemoveFile(Window window) 101 | { 102 | if (!string.IsNullOrWhiteSpace(SelectedPlugin)) 103 | { 104 | ListerPlugins?.Remove(SelectedPlugin); 105 | } 106 | } 107 | 108 | public static FilePickerFileType ListerPluginsFilter { get; } = new("Lister Plugins (64bit)") 109 | { 110 | Patterns = new[] { /*"*.wlx",*/ "*.wlx64" } 111 | }; 112 | private void AddFileAsync(Window window) 113 | { 114 | var desktop = (IClassicDesktopStyleApplicationLifetime?)Application.Current?.ApplicationLifetime; 115 | var topLevel = TopLevel.GetTopLevel(desktop?.MainWindow); 116 | var files = topLevel?.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions 117 | { 118 | Title = "Choose plugin", 119 | AllowMultiple = false, 120 | FileTypeFilter = new[] { ListerPluginsFilter } 121 | }).Result; 122 | 123 | if (files?.Count >= 1) 124 | { 125 | var filename = files.First().Path.LocalPath; 126 | 127 | if (ListerPlugins.IndexOf(filename)==-1) 128 | ListerPlugins.Add(filename); 129 | } 130 | } 131 | 132 | public void SaveClose(Window window) 133 | { 134 | Model.IsCurrentDirectoryDisplayed = IsCurrentDirectoryDisplayed; 135 | Model.IsFunctionKeysDisplayed = IsFunctionKeysDisplayed; 136 | Model.IsCommandLineDisplayed = IsCommandLineDisplayed; 137 | Model.IsHiddenSystemFilesDisplayed = IsHiddenSystemFilesDisplayed; 138 | Model.SaveSettingsOnExit = SaveSettingsOnExit; 139 | Model.ConfirmationWhenDeleteNonEmpty = ConfirmationWhenDeleteNonEmpty; 140 | Model.SaveWindowPositionSize = SaveWindowPositionSize; 141 | Model.IsDarkThemeEnabled = IsDarkThemeEnabled; 142 | Model.Language = SelectedCulture.Name; 143 | Model.AllowOnlyOneInstance = AllowOnlyOneInstance; 144 | Model.ListerPlugins = ListerPlugins.ToList(); 145 | 146 | Model.Save(); 147 | window?.Close(this); 148 | 149 | } 150 | 151 | public void Close(Window window) 152 | { 153 | window?.Close(); 154 | } 155 | 156 | private OptionsModel Model => OptionsModel.Instance; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | -------------------------------------------------------------------------------- /src/SmartCommander/Utils.cs: -------------------------------------------------------------------------------- 1 | using SmartCommander.Models; 2 | using SmartCommander.ViewModels; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading; 9 | 10 | namespace SmartCommander 11 | { 12 | static internal class Utils 13 | { 14 | static int oldProgressValue = 0; 15 | static internal void ReportProgress(IProgress? progress, long processedSize, long totalSize) 16 | { 17 | if (progress == null) 18 | { 19 | return; 20 | } 21 | int newValue = totalSize == 0 ? 0 : Convert.ToInt32(processedSize * 100 / totalSize); 22 | if (newValue != oldProgressValue) 23 | { 24 | oldProgressValue = newValue; 25 | progress?.Report(newValue); 26 | } 27 | } 28 | static internal void DeleteDirectoryWithHiddenFiles(string path) 29 | { 30 | if (!Directory.Exists(path)) 31 | { 32 | return; 33 | } 34 | var directory = new DirectoryInfo(path) { Attributes = FileAttributes.Normal }; 35 | 36 | foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories)) 37 | { 38 | info.Attributes = FileAttributes.Normal; 39 | } 40 | 41 | directory.Delete(true); 42 | } 43 | 44 | static internal void SetNormalFileAttributes(string path) 45 | { 46 | if (!File.Exists(path)) 47 | { 48 | return; 49 | } 50 | FileInfo fileInfo = new FileInfo(path); 51 | fileInfo.Attributes = FileAttributes.Normal; 52 | 53 | } 54 | 55 | static internal long GetTotalSize(List selectedItems) 56 | { 57 | long totalSize = 0; 58 | foreach (var item in selectedItems) 59 | { 60 | string path = item.FullName; 61 | if (item.IsFolder) 62 | { 63 | totalSize += GetDirectorySize(new DirectoryInfo(path)); 64 | } 65 | else 66 | { 67 | totalSize += new FileInfo(path).Length; 68 | } 69 | } 70 | return totalSize; 71 | } 72 | 73 | private static long GetDirectorySize(DirectoryInfo d) 74 | { 75 | long size = 0; 76 | // Add file sizes. 77 | FileInfo[] fis = d.GetFiles(); 78 | foreach (FileInfo fi in fis) 79 | { 80 | size += fi.Length; 81 | } 82 | // Add subdirectory sizes. 83 | DirectoryInfo[] dis = d.GetDirectories(); 84 | foreach (DirectoryInfo di in dis) 85 | { 86 | size += GetDirectorySize(di); 87 | } 88 | return size; 89 | } 90 | 91 | static internal List GetDuplicates(List selectedItems, string destpath) 92 | { 93 | var duplicates = new List(); 94 | 95 | foreach (var item in selectedItems) 96 | { 97 | string targetFilePath = Path.Combine(destpath, Path.GetFileName(item.FullName)); 98 | if (item.IsFolder) 99 | { 100 | EnumerateDuplicates(item.FullName, targetFilePath, ref duplicates); 101 | } 102 | else 103 | { 104 | if (File.Exists(targetFilePath)) 105 | { 106 | duplicates.Add(item.FullName); 107 | } 108 | } 109 | } 110 | 111 | return duplicates; 112 | } 113 | 114 | static internal List GetNonEmptyFolders(List selectedItems) 115 | { 116 | var nonEmptyFolders = new List(); 117 | 118 | if (!OptionsModel.Instance.ConfirmationWhenDeleteNonEmpty) 119 | { 120 | return nonEmptyFolders; 121 | } 122 | 123 | foreach (var item in selectedItems) 124 | { 125 | if (item.IsFolder && !IsDirectoryEmpty(item.FullName)) 126 | { 127 | nonEmptyFolders.Add(item.FullName); 128 | } 129 | } 130 | 131 | return nonEmptyFolders; 132 | } 133 | 134 | static internal bool IsDirectoryEmpty(string path) 135 | { 136 | return !Directory.EnumerateFileSystemEntries(path).Any(); 137 | } 138 | 139 | static internal void CopyFile(string source, 140 | string dest, 141 | bool delete, 142 | bool overwrite, 143 | CancellationToken ct, 144 | IProgress? progress, 145 | ref long processedSize, 146 | long totalSize) 147 | { 148 | long size = new FileInfo(source).Length; 149 | processedSize += size; 150 | if (ct.IsCancellationRequested) 151 | { 152 | ct.ThrowIfCancellationRequested(); 153 | } 154 | 155 | if (!overwrite) 156 | { 157 | if (File.Exists(dest)) 158 | { 159 | return; 160 | } 161 | } 162 | 163 | if (delete) 164 | { 165 | if (OperatingSystem.IsWindows()) 166 | { 167 | if (Path.GetPathRoot(source) == Path.GetPathRoot(dest)) 168 | { 169 | File.Move(source, dest,overwrite); 170 | return; 171 | } 172 | } 173 | } 174 | 175 | const int bufferSize = 1024 * 1024; // 1MB 176 | const long limit = 10 * bufferSize; 177 | 178 | if (size > limit) 179 | { 180 | using (Stream from = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.Write)) 181 | using (Stream to = new FileStream(dest, FileMode.OpenOrCreate)) 182 | { 183 | // TODO: report progress by chunks 184 | int readCount; 185 | byte[] buffer = new byte[bufferSize]; 186 | while ((readCount = from.Read(buffer, 0, bufferSize)) != 0) 187 | { 188 | if (ct.IsCancellationRequested) 189 | { 190 | ct.ThrowIfCancellationRequested(); 191 | } 192 | to.Write(buffer, 0, readCount); 193 | } 194 | } 195 | } 196 | else 197 | { 198 | File.Copy(source, dest, overwrite); 199 | } 200 | 201 | if (delete) 202 | { 203 | File.Delete(source); 204 | } 205 | Utils.ReportProgress(progress, processedSize, totalSize); 206 | } 207 | 208 | static internal void CopyDirectory(string sourceDir, 209 | string destinationDir, 210 | bool recursive, 211 | bool overwrite, 212 | CancellationToken ct, 213 | IProgress? progress, 214 | ref long processedSize, 215 | long totalSize) 216 | { 217 | if (ct.IsCancellationRequested) 218 | { 219 | ct.ThrowIfCancellationRequested(); 220 | } 221 | // Get information about the source directory 222 | var dir = new DirectoryInfo(sourceDir); 223 | 224 | // Check if the source directory exists 225 | if (!dir.Exists) 226 | throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); 227 | 228 | // Cache directories before we start copying 229 | DirectoryInfo[] dirs = dir.GetDirectories(); 230 | 231 | // Create the destination directory 232 | Directory.CreateDirectory(destinationDir); 233 | 234 | // Get the files in the source directory and copy to the destination directory 235 | foreach (FileInfo file in dir.GetFiles()) 236 | { 237 | if (ct.IsCancellationRequested) 238 | { 239 | ct.ThrowIfCancellationRequested(); 240 | } 241 | string targetFilePath = Path.Combine(destinationDir, file.Name); 242 | if (File.Exists(targetFilePath)) 243 | { 244 | Utils.SetNormalFileAttributes(targetFilePath); 245 | } 246 | CopyFile(file.FullName, targetFilePath, delete: false, overwrite, ct, 247 | progress, ref processedSize, totalSize); 248 | } 249 | 250 | // If recursive and copying subdirectories, recursively call this method 251 | if (recursive) 252 | { 253 | foreach (DirectoryInfo subDir in dirs) 254 | { 255 | if (ct.IsCancellationRequested) 256 | { 257 | ct.ThrowIfCancellationRequested(); 258 | } 259 | string newDestinationDir = Path.Combine(destinationDir, subDir.Name); 260 | CopyDirectory(subDir.FullName, newDestinationDir, true, overwrite, ct, 261 | progress, ref processedSize, totalSize); 262 | } 263 | } 264 | } 265 | 266 | static internal void EnumerateDuplicates(string sourceDir, string destinationDir, ref List duplicates) 267 | { 268 | // Get information about the source directory 269 | var dir = new DirectoryInfo(sourceDir); 270 | 271 | // Check if the source directory exists 272 | if (!dir.Exists) 273 | throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); 274 | 275 | // Cache directories before we start copying 276 | DirectoryInfo[] dirs = dir.GetDirectories(); 277 | 278 | // Get the files in the source directory and copy to the destination directory 279 | foreach (FileInfo file in dir.GetFiles()) 280 | { 281 | string targetFilePath = Path.Combine(destinationDir, file.Name); 282 | if (File.Exists(targetFilePath)) 283 | { 284 | duplicates.Add(file.FullName); 285 | } 286 | } 287 | 288 | foreach (DirectoryInfo subDir in dirs) 289 | { 290 | string newDestinationDir = Path.Combine(destinationDir, subDir.Name); 291 | EnumerateDuplicates(subDir.FullName, newDestinationDir, ref duplicates); 292 | } 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/SmartCommander/Assets/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Alert 122 | 123 | 124 | Archive {0} already exists. 125 | 126 | 127 | Cancel 128 | 129 | 130 | Can't copy files to the same directory 131 | 132 | 133 | Can't edit the folder 134 | 135 | 136 | Can't move files to the same directory 137 | 138 | 139 | Can't move folder here 140 | 141 | 142 | Can't move folder to itself 143 | 144 | 145 | Can't view the folder 146 | 147 | 148 | Copy {0} to: 149 | 150 | 151 | Create New Folder 152 | 153 | 154 | Current directory name 155 | 156 | 157 | Files: {0}, folders: {1}. 158 | 159 | 160 | Dark Theme 161 | 162 | 163 | Date 164 | 165 | 166 | Are you sure you would like to delete {0}? 167 | 168 | 169 | The folder is not empty. Are you sure you would like to delete {0}? 170 | 171 | 172 | Display Command Line 173 | 174 | 175 | Display Current Directory 176 | 177 | 178 | Display Function Keys 179 | 180 | 181 | The drive is not available 182 | 183 | 184 | Edit 185 | 186 | 187 | Error: no pane selected 188 | 189 | 190 | Extension 191 | 192 | 193 | F3 View 194 | 195 | 196 | F4 Edit 197 | 198 | 199 | F5 Copy 200 | 201 | 202 | F6 Move 203 | 204 | 205 | F7 Create Folder 206 | 207 | 208 | F8 Delete 209 | 210 | 211 | File already exists. Are you sure you would like to rewrite {0}? 212 | 213 | 214 | Folder 215 | 216 | 217 | The folder already exists 218 | 219 | 220 | {0} items 221 | 222 | 223 | Light Theme 224 | 225 | 226 | _Configuration 227 | 228 | 229 | _Exit 230 | 231 | 232 | _File 233 | 234 | 235 | _Options 236 | 237 | 238 | Sort by _date 239 | 240 | 241 | Sort by _extension 242 | 243 | 244 | Sort by _name 245 | 246 | 247 | Sort by _size 248 | 249 | 250 | _View 251 | 252 | 253 | Move {0} to: 254 | 255 | 256 | Name 257 | 258 | 259 | Not Found: 260 | 261 | 262 | OK 263 | 264 | 265 | Operation in Progress: 266 | 267 | 268 | Options 269 | 270 | 271 | Save 272 | 273 | 274 | Save Settings on Exit 275 | 276 | 277 | Save Window Position and Size 278 | 279 | 280 | Show Hidden/System Files 281 | 282 | 283 | Show Confirmation when Deleting Non-empty Directory 284 | 285 | 286 | Size 287 | 288 | 289 | Would you like to stop the background operation? 290 | 291 | 292 | View 293 | 294 | 295 | Zip 296 | 297 | 298 | Unzip 299 | 300 | 301 | Directory {0} already exists. 302 | 303 | 304 | Too large file size for viewing. 305 | 306 | 307 | *Application restart required 308 | 309 | 310 | Allow only one Instance 311 | 312 | 313 | Files Search 314 | 315 | 316 | Start Search 317 | 318 | 319 | Current Folder 320 | 321 | 322 | File name (for example, *.txt) 323 | 324 | 325 | _Search 326 | 327 | 328 | Search in top directory only 329 | 330 | 331 | Search in file content 332 | 333 | 334 | Header 335 | 336 | 337 | Operation 338 | 339 | 340 | Language 341 | 342 | 343 | Plugins 344 | 345 | -------------------------------------------------------------------------------- /src/SmartCommander/Assets/Resources.ru-RU.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Оповещение 122 | 123 | 124 | Архив {0} уже существует. 125 | 126 | 127 | Отмена 128 | 129 | 130 | Невозможно скопировать файлы в ту же директорию 131 | 132 | 133 | Невозможно редактировать папку 134 | 135 | 136 | Невозможно переместить файлы в ту же директорию 137 | 138 | 139 | Невозможно переместить папку сюда 140 | 141 | 142 | Невозможно переместить папку в саму себя 143 | 144 | 145 | Невозможно просмотреть папку 146 | 147 | 148 | Скопировать {0} в: 149 | 150 | 151 | Создать новую папку 152 | 153 | 154 | Текущее имя директории 155 | 156 | 157 | Файлов: {0}, папок: {1}. 158 | 159 | 160 | Тёмная тема 161 | 162 | 163 | Дата 164 | 165 | 166 | Вы уверены, что хотите удалить {0}? 167 | 168 | 169 | Папка не пуста. Вы уверены, что хотите удалить {0}? 170 | 171 | 172 | Отобразить командную строку 173 | 174 | 175 | Отобразить текущую директорию 176 | 177 | 178 | Отобразить функциональные клавиши 179 | 180 | 181 | Диск недоступен 182 | 183 | 184 | Редактировать 185 | 186 | 187 | Ошибка: панель не выбрана 188 | 189 | 190 | Расширение 191 | 192 | 193 | F3 Просмотр 194 | 195 | 196 | F4 Редактирование 197 | 198 | 199 | F5 Копировать 200 | 201 | 202 | F6 Переместить 203 | 204 | 205 | F7 Создать папку 206 | 207 | 208 | F8 Удалить 209 | 210 | 211 | Файл уже существует. Вы уверены, что хотите перезаписать {0}? 212 | 213 | 214 | Папка 215 | 216 | 217 | Папка уже существует 218 | 219 | 220 | {0} элементов 221 | 222 | 223 | Светлая тема 224 | 225 | 226 | _Конфигурация 227 | 228 | 229 | _Выход 230 | 231 | 232 | _Файл 233 | 234 | 235 | _Опции 236 | 237 | 238 | Сортировать по _дате 239 | 240 | 241 | Сортировать по _расширению 242 | 243 | 244 | Сортировать по _имени 245 | 246 | 247 | Сортировать по _размеру 248 | 249 | 250 | _Вид 251 | 252 | 253 | Переместить {0} в: 254 | 255 | 256 | Имя 257 | 258 | 259 | Не найдено: 260 | 261 | 262 | ОК 263 | 264 | 265 | Операция в процессе: 266 | 267 | 268 | Опции 269 | 270 | 271 | Сохранить 272 | 273 | 274 | Сохранить настройки при выходе 275 | 276 | 277 | Сохранить положение и размер окна 278 | 279 | 280 | Показать скрытые/системные файлы 281 | 282 | 283 | Показать подтверждение при удалении непустой папки 284 | 285 | 286 | Размер 287 | 288 | 289 | Хотите остановить фоновую операцию? 290 | 291 | 292 | Просмотр 293 | 294 | 295 | Архивировать 296 | 297 | 298 | Разархивировать 299 | 300 | 301 | Директория {0} уже существует. 302 | 303 | 304 | Слишком большой размер файла для просмотра. 305 | 306 | 307 | *Требуется перезапуск приложения 308 | 309 | 310 | Разрешать только один экземпляр приложения 311 | 312 | 313 | Поиск по файлам 314 | 315 | 316 | Начать поиск 317 | 318 | 319 | Текущая папка 320 | 321 | 322 | Имя файла (например, *.txt) 323 | 324 | 325 | _Поиск 326 | 327 | 328 | Искать только в основной папке 329 | 330 | 331 | Искать в содержимом файлов 332 | 333 | 334 | Заголовок 335 | 336 | 337 | Операция 338 | 339 | 340 | Язык 341 | 342 | 343 | Плагины 344 | 345 | -------------------------------------------------------------------------------- /src/SmartCommander/Assets/Resources.sr-Cyrl-RS.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Упозорење 122 | 123 | 124 | Архива {0} већ постоји. 125 | 126 | 127 | Откажи 128 | 129 | 130 | Не можете да копираjте датотеке у исту фасциклу 131 | 132 | 133 | Не можете да уређиваjте фасциклу 134 | 135 | 136 | Не можете да преместите датотеке у исту фасциклу 137 | 138 | 139 | Овде не можете да преместите фасциклу 140 | 141 | 142 | Не можете да премјестите фасциклу овде 143 | 144 | 145 | Не можете да прегледаjте фасциклу 146 | 147 | 148 | Копираj {0} у: 149 | 150 | 151 | Креирај нову фасциклу 152 | 153 | 154 | Назив тренутног директоријума 155 | 156 | 157 | Датотеке: {0}, фасцикле: {1}. 158 | 159 | 160 | Тамна тема 161 | 162 | 163 | Датум 164 | 165 | 166 | Да ли сте сигурни да желите да обришете {0}? 167 | 168 | 169 | Фасцикла није празна. Да ли сте сигурни да желите да избришете {0}? 170 | 171 | 172 | Прикажи командну линију 173 | 174 | 175 | Прикажи тренутну фасциклу 176 | 177 | 178 | Прикажи функционалне типке 179 | 180 | 181 | Диск није доступан 182 | 183 | 184 | Уреди 185 | 186 | 187 | Грешка: ниједан панел није одабран 188 | 189 | 190 | Екстензија 191 | 192 | 193 | F3 Прегледај 194 | 195 | 196 | F4 Уреди 197 | 198 | 199 | F5 Копирај 200 | 201 | 202 | F6 Премјести 203 | 204 | 205 | F7 Креирај фасциклу 206 | 207 | 208 | F8 Обриши 209 | 210 | 211 | Датотека већ постоји. Да ли сте сигурни да желите да препишете {0}? 212 | 213 | 214 | Фасцикла 215 | 216 | 217 | Фасцикла већ постоји 218 | 219 | 220 | {0} елемената 221 | 222 | 223 | Светла тема 224 | 225 | 226 | _Конфигурација 227 | 228 | 229 | _Излаз 230 | 231 | 232 | _Датотека 233 | 234 | 235 | _Опције 236 | 237 | 238 | Сортирај по _датуму 239 | 240 | 241 | Сортирај по _екстензији 242 | 243 | 244 | Сортираj по _називу датотеке 245 | 246 | 247 | Сортирај по _величини датотеке 248 | 249 | 250 | _Прегледај 251 | 252 | 253 | Премјести {0} у: 254 | 255 | 256 | Назив 257 | 258 | 259 | Није пронађен: 260 | 261 | 262 | ОК 263 | 264 | 265 | Операција у току: 266 | 267 | 268 | Опције 269 | 270 | 271 | Сачувај 272 | 273 | 274 | Сачувај подешавања при изласку 275 | 276 | 277 | Сачувај позицију и величину прозора апликације 278 | 279 | 280 | Прикажи скривене/системске датотеке 281 | 282 | 283 | Прикажи потврду приликом брисања директоријума који није празан 284 | 285 | 286 | Величина 287 | 288 | 289 | Да ли желите да зауставите операцију у току? 290 | 291 | 292 | Прегледај 293 | 294 | 295 | Зип 296 | 297 | 298 | Распакуj 299 | 300 | 301 | Фасцикла {0} већ постоји. 302 | 303 | 304 | Величина датотеке је превелика за преглед. 305 | 306 | 307 | *Потребно је поновно покретање апликација 308 | 309 | 310 | Дозволи само једну инстанцу апликације 311 | 312 | 313 | Претрага датoтека 314 | 315 | 316 | Започети претрагу 317 | 318 | 319 | Тренутни директоријум 320 | 321 | 322 | Назив датотеке (на пример, *.txt) 323 | 324 | 325 | _Тражи 326 | 327 | 328 | Тражи само у горњем директоријуму 329 | 330 | 331 | Тражи садржај датотеке 332 | 333 | 334 | Заглавље 335 | 336 | 337 | Операција 338 | 339 | 340 | Језик 341 | 342 | 343 | Допуне 344 | 345 | -------------------------------------------------------------------------------- /src/SmartCommander/Assets/Resources.sr-Latn-ME.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Upozorenje 122 | 123 | 124 | Arhiva {0} već postoji. 125 | 126 | 127 | Otkaži 128 | 129 | 130 | Ne možete da kopirajte datoteke u istu fasciklu 131 | 132 | 133 | Ne možete da uređivajte fasciklu 134 | 135 | 136 | Ne možete da premjestite datoteke u istu fasciklu 137 | 138 | 139 | Ne možete da premjestite fasciklu ovdje 140 | 141 | 142 | Ne možete da premjestite fasciklu ovdje 143 | 144 | 145 | Ne možete da pregledajte fasciklu 146 | 147 | 148 | Kopiraj {0} u: 149 | 150 | 151 | Kreiraj novu fasciklu 152 | 153 | 154 | Naziv trenutnog direktorijuma 155 | 156 | 157 | Datoteke: {0}, fascikle: {1}. 158 | 159 | 160 | Tamna tema 161 | 162 | 163 | Datum 164 | 165 | 166 | Da li ste sigurni da želite da obrišete {0}? 167 | 168 | 169 | Fascikla nije prazna. Da li ste sigurni da želite obrisati {0}? 170 | 171 | 172 | Prikaži komandnu liniju 173 | 174 | 175 | Prikaži trenutnu fasciklu 176 | 177 | 178 | Prikaži funkcijske tipke 179 | 180 | 181 | Disk nije dostupan 182 | 183 | 184 | Uredi 185 | 186 | 187 | Greška: nijedan panel nije odabran 188 | 189 | 190 | Ekstenzija 191 | 192 | 193 | F3 Pregledaj 194 | 195 | 196 | F4 Uredi 197 | 198 | 199 | F5 Kopiruj 200 | 201 | 202 | F6 Premjesti 203 | 204 | 205 | F7 Kreiraj fasciklu 206 | 207 | 208 | F8 Obriši 209 | 210 | 211 | Datoteka već postoji. Da li ste sigurni da želite prepisati {0}? 212 | 213 | 214 | Fascikla 215 | 216 | 217 | Fascikla već postoji 218 | 219 | 220 | {0} elemenata 221 | 222 | 223 | Svijetla tema 224 | 225 | 226 | _Konfiguracija 227 | 228 | 229 | _Izlaz 230 | 231 | 232 | _Datoteka 233 | 234 | 235 | _Opcije 236 | 237 | 238 | Sortiraj po _datumu 239 | 240 | 241 | Sortiraj po _ekstenziji 242 | 243 | 244 | Sortiraj po _nazivu datoteke 245 | 246 | 247 | Sortiraj po _veličini datoteke 248 | 249 | 250 | _Pregledaj 251 | 252 | 253 | Premjesti {0} to: 254 | 255 | 256 | Naziv 257 | 258 | 259 | Nije pronađen: 260 | 261 | 262 | OK 263 | 264 | 265 | Operacija u toku: 266 | 267 | 268 | Opcije 269 | 270 | 271 | Sačuvaj 272 | 273 | 274 | Sačuvaj podešavanja pri izlasku 275 | 276 | 277 | Sačuvaj poziciju i veličinu prozora aplikacije 278 | 279 | 280 | Prikaži skrivene/sistemske datoteke 281 | 282 | 283 | Prikaži potvrdu prilikom brisanja direktorijuma koji nije prazan 284 | 285 | 286 | Veličina 287 | 288 | 289 | Da li želite da zaustavite operaciju u toku? 290 | 291 | 292 | Pregledaj 293 | 294 | 295 | Zip 296 | 297 | 298 | Raspakuj 299 | 300 | 301 | Fascikla {0} već postoji. 302 | 303 | 304 | Veličina datoteke je prevelika za pregled. 305 | 306 | 307 | *Potrebno je ponovno pokretanje aplikacije 308 | 309 | 310 | Dozvoli samo jednu instancu aplikacije 311 | 312 | 313 | Pretraga datoteka 314 | 315 | 316 | Započeti pretragu 317 | 318 | 319 | Trenutni direktorijum 320 | 321 | 322 | Naziv datoteke (na primer, *.txt) 323 | 324 | 325 | _Traži 326 | 327 | 328 | Traži samo u gornjem direktorijumu 329 | 330 | 331 | Traži sadržaj datoteke 332 | 333 | 334 | Zaglavlje 335 | 336 | 337 | Operacija 338 | 339 | 340 | Jezik 341 | 342 | 343 | Dopune 344 | 345 | -------------------------------------------------------------------------------- /src/SmartCommander/Assets/Resources.fr-FR.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Alerte 122 | 123 | 124 | L'archive {0} existe déjà. 125 | 126 | 127 | Annuler 128 | 129 | 130 | Impossible de copier les fichiers dans le même dossier 131 | 132 | 133 | Impossible d'éditer le dossier 134 | 135 | 136 | Impossible de déplacer les fichiers dans le même dossier 137 | 138 | 139 | Impossible de déplacer le dossier ici 140 | 141 | 142 | Impossible de déplacer le dossier vers lui-même 143 | 144 | 145 | Impossible d'afficher le dossier 146 | 147 | 148 | Copier {0} vers : 149 | 150 | 151 | Créer un nouveau dossier 152 | 153 | 154 | Nom du répertoire courant 155 | 156 | 157 | Fichiers : {0}, dossiers : {1}. 158 | 159 | 160 | Thème sombre 161 | 162 | 163 | Date 164 | 165 | 166 | Voulez-vous vraiment supprimer {0}? 167 | 168 | 169 | Le dossier n'est pas vide. Êtes-vous sûr de vouloir supprimer {0}? 170 | 171 | 172 | Afficher la ligne de commande 173 | 174 | 175 | Afficher le dossier courant 176 | 177 | 178 | Afficher les touches de fonction 179 | 180 | 181 | Le disque n'est pas disponible 182 | 183 | 184 | Modifier 185 | 186 | 187 | Erreur: aucun volet sélectionné 188 | 189 | 190 | Extension 191 | 192 | 193 | F3 Afficher 194 | 195 | 196 | F4 Modifier 197 | 198 | 199 | F5 Copier 200 | 201 | 202 | F6 Coup 203 | 204 | 205 | F7 Créer un dossier 206 | 207 | 208 | F8 Supprimer 209 | 210 | 211 | Le fichier existe déjà. Êtes-vous sûr de vouloir réécrire {0}? 212 | 213 | 214 | Dossier 215 | 216 | 217 | Le dossier existe déjà 218 | 219 | 220 | {0} éléments 221 | 222 | 223 | Thème clair 224 | 225 | 226 | _Configuration 227 | 228 | 229 | _Quitter 230 | 231 | 232 | _Fichier 233 | 234 | 235 | _Paramètres 236 | 237 | 238 | Trier par _date 239 | 240 | 241 | Trier par _extension 242 | 243 | 244 | Trier par _nom 245 | 246 | 247 | Trier par _taille 248 | 249 | 250 | _Afficher 251 | 252 | 253 | Déplacer {0} vers : 254 | 255 | 256 | Nom 257 | 258 | 259 | Non trouvé : 260 | 261 | 262 | OK 263 | 264 | 265 | Opération en cours: 266 | 267 | 268 | Paramètres 269 | 270 | 271 | Sauvegarder 272 | 273 | 274 | Sauvegarder les paramètres en quittant 275 | 276 | 277 | Sauvegarder la position et la taille de la fenêtre 278 | 279 | 280 | Afficher les fichiers cachés/système 281 | 282 | 283 | Afficher la confirmation lors de la suppression du répertoire non-vide 284 | 285 | 286 | Taille 287 | 288 | 289 | Voulez-vous arrêter l'opération en arrière-plan? 290 | 291 | 292 | Afficher 293 | 294 | 295 | Compresser 296 | 297 | 298 | Décompresser 299 | 300 | 301 | Le dossier {0} existe déjà. 302 | 303 | 304 | La taille du fichier est trop grande pour être affichée. 305 | 306 | 307 | Redémarrage d'application requis 308 | 309 | 310 | Autoriser une seule instance 311 | 312 | 313 | Recherche de fichiers 314 | 315 | 316 | Lancer la recherche 317 | 318 | 319 | Dossier courant 320 | 321 | 322 | Nom du fichier (par exemple, *.txt) 323 | 324 | 325 | _Rechercher 326 | 327 | 328 | Rechercher dans le répertoire supérieur uniquement 329 | 330 | 331 | Rechercher dans le contenu du fichier 332 | 333 | 334 | En-tête 335 | 336 | 337 | Opération 338 | 339 | 340 | Langue 341 | 342 | 343 | Plugins 344 | 345 | -------------------------------------------------------------------------------- /src/SmartCommander/ViewModels/FilesPaneViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Input; 3 | using Avalonia.Media; 4 | using MsBox.Avalonia.Enums; 5 | using ReactiveUI; 6 | using SmartCommander.Assets; 7 | using SmartCommander.Models; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Collections.ObjectModel; 11 | using System.Diagnostics; 12 | using System.IO; 13 | using System.Linq; 14 | using System.Reactive; 15 | using System.Reactive.Linq; 16 | using System.Runtime.InteropServices; 17 | using System.Threading.Tasks; 18 | using System.Windows.Input; 19 | using Path = System.IO.Path; 20 | 21 | namespace SmartCommander.ViewModels 22 | { 23 | public enum SortingBy 24 | { 25 | SortingByName = 0, 26 | SortingByExt, 27 | SortingBySize, 28 | SortingByDate, 29 | } 30 | 31 | public class FilesPaneViewModel : ViewModelBase 32 | { 33 | private string _currentDirectory = ""; 34 | 35 | private int _totalFiles = 0; 36 | private int _totalFolders = 0; 37 | 38 | private bool _isSelected; 39 | private SortingBy _sorting = SortingBy.SortingByName; 40 | private bool _ascending = true; 41 | public event EventHandler? FocusChanged; 42 | 43 | public string CurrentDirectory 44 | { 45 | get => _currentDirectory; 46 | set 47 | { 48 | _currentDirectory = value; 49 | GetFilesFolders(CurrentDirectory, FoldersFilesList); 50 | this.RaisePropertyChanged(nameof(CurrentDirectory)); 51 | this.RaisePropertyChanged(nameof(CurrentDirectoryInfo)); 52 | } 53 | } 54 | 55 | 56 | private MainWindowViewModel _mainVM; 57 | 58 | public string CurrentDirectoryInfo => string.Format(Resources.CurrentDirInfo, _totalFiles, _totalFolders); 59 | 60 | public FileViewModel? _currentItem; 61 | public FileViewModel? CurrentItem 62 | { 63 | get => _currentItem; 64 | set => this.RaiseAndSetIfChanged(ref _currentItem, value); 65 | } 66 | 67 | public List CurrentItems { get; set; } = new List(); 68 | 69 | public bool IsUnzip => CurrentItems.Count > 0 && CurrentItems[0].Extension == "zip"; 70 | 71 | 72 | public SortingBy Sorting 73 | { 74 | get => _sorting; 75 | set 76 | { 77 | _sorting = value; 78 | Update(); 79 | } 80 | } 81 | 82 | public bool Ascending 83 | { 84 | 85 | get => _ascending; 86 | set 87 | { 88 | _ascending = value; 89 | Update(); 90 | } 91 | } 92 | 93 | public bool IsSelected 94 | { 95 | get => _isSelected; 96 | set 97 | { 98 | _isSelected = value; 99 | this.RaisePropertyChanged(nameof(GridBorderBrush)); 100 | 101 | if (value) 102 | { 103 | FocusChanged?.Invoke(this, EventArgs.Empty); 104 | } 105 | } 106 | } 107 | 108 | 109 | public static bool IsCurrentDirectoryDisplayed 110 | { 111 | get => OptionsModel.Instance.IsCurrentDirectoryDisplayed; 112 | } 113 | 114 | 115 | public static Brush SelectedBrush = new SolidColorBrush(Colors.LightSkyBlue); 116 | public static Brush NotSelectedBrush = new SolidColorBrush(Colors.Transparent); 117 | public Brush GridBorderBrush => IsSelected ? SelectedBrush : NotSelectedBrush; 118 | 119 | public ObservableCollection FoldersFilesList { get; set; } = new ObservableCollection(); 120 | 121 | public FilesPaneViewModel() 122 | { 123 | _mainVM = new MainWindowViewModel(); 124 | ShowViewerDialog = new Interaction(); 125 | } 126 | 127 | public FilesPaneViewModel(MainWindowViewModel mainVM, EventHandler focusHandler) 128 | { 129 | CurrentDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Personal); 130 | ViewCommand = ReactiveCommand.Create(View); 131 | EditCommand = ReactiveCommand.Create(Edit); 132 | ZipCommand = ReactiveCommand.Create(Zip); 133 | UnzipCommand = ReactiveCommand.Create(Unzip); 134 | ShowViewerDialog = new Interaction(); 135 | _mainVM = mainVM; 136 | FocusChanged += focusHandler; 137 | } 138 | 139 | public event Action? ScrollToItemRequested; 140 | 141 | public void RequestScroll(object item, object? column) 142 | { 143 | ScrollToItemRequested?.Invoke(item, column!); 144 | } 145 | public ReactiveCommand? ViewCommand { get; } 146 | public ReactiveCommand? EditCommand { get; } 147 | public ReactiveCommand? ZipCommand { get; } 148 | public ReactiveCommand? UnzipCommand { get; } 149 | 150 | public Interaction ShowViewerDialog { get; } 151 | 152 | public void CellPointerPressed(object sender, object parameter) 153 | { 154 | _mainVM.SelectedPane = this; 155 | } 156 | 157 | public void SortingStarted(object sender, object parameter) 158 | { 159 | _mainVM.SelectedPane = this; 160 | 161 | DataGridColumnEventArgs? args = parameter as DataGridColumnEventArgs; 162 | if (args != null) 163 | { 164 | var header = args.Column.Header.ToString(); 165 | if (header == Resources.Name) 166 | { 167 | if (Sorting == SortingBy.SortingByName) 168 | { 169 | Ascending = !Ascending; 170 | } 171 | else 172 | { 173 | Sorting = SortingBy.SortingByName; 174 | Ascending = true; 175 | } 176 | } 177 | if (header == Resources.Extension) 178 | { 179 | if (Sorting == SortingBy.SortingByExt) 180 | { 181 | Ascending = !Ascending; 182 | } 183 | else 184 | { 185 | Sorting = SortingBy.SortingByExt; 186 | Ascending = true; 187 | } 188 | } 189 | if (header == Resources.Size) 190 | { 191 | if (Sorting == SortingBy.SortingBySize) 192 | { 193 | Ascending = !Ascending; 194 | } 195 | else 196 | { 197 | Sorting = SortingBy.SortingBySize; 198 | Ascending = true; 199 | } 200 | } 201 | if (header == Resources.Date) 202 | { 203 | if (Sorting == SortingBy.SortingByDate) 204 | { 205 | Ascending = !Ascending; 206 | } 207 | else 208 | { 209 | Sorting = SortingBy.SortingByDate; 210 | Ascending = true; 211 | } 212 | } 213 | 214 | args.Handled = true; 215 | } 216 | } 217 | 218 | public void BeginningEdit(object sender, object parameter) 219 | { 220 | DataGridBeginningEditEventArgs? args = parameter as DataGridBeginningEditEventArgs; 221 | if (args != null && 222 | args.Column.DisplayIndex != 1) 223 | { 224 | args.Cancel = true; 225 | } 226 | if (args != null && 227 | CurrentItem != null && 228 | CurrentItem.FullName == "..") 229 | { 230 | args.Cancel = true; 231 | } 232 | } 233 | 234 | public void SelectionChanged(object sender, object parameter) 235 | { 236 | SelectionChangedEventArgs? args = parameter as SelectionChangedEventArgs; 237 | if (args != null) 238 | { 239 | DataGrid? grid = args.Source as DataGrid; 240 | if (grid != null) 241 | { 242 | // non-bindable property 243 | CurrentItems = grid.SelectedItems.Cast().ToList(); 244 | ///grid.SelectedIndex = 0; 245 | } 246 | } 247 | } 248 | 249 | public void Tapped(object sender, object parameter) 250 | { 251 | _mainVM.SelectedPane = this; 252 | } 253 | 254 | public void DoubleTapped(object sender, object parameter) 255 | { 256 | var args = parameter as TappedEventArgs; 257 | if (args != null) 258 | { 259 | var source = args.Source as Control; 260 | if (source != null && 261 | (source.TemplatedParent is DataGridCell || source.Parent is DataGridCell)) 262 | { 263 | ProcessCurrentItem(); 264 | } 265 | } 266 | } 267 | 268 | public void Execute(string? command) 269 | { 270 | if (!string.IsNullOrEmpty(command)) 271 | { 272 | new Process 273 | { 274 | StartInfo = new ProcessStartInfo(command) 275 | { 276 | WorkingDirectory = CurrentDirectory, 277 | UseShellExecute = true 278 | } 279 | }.Start(); 280 | } 281 | } 282 | 283 | public void Update() 284 | { 285 | // TODO: may be there is anything smarter? 286 | CurrentDirectory = CurrentDirectory; 287 | } 288 | 289 | public void View() 290 | { 291 | _ = View(null); 292 | } 293 | 294 | public async Task View(Action? resultAction) 295 | { 296 | if (CurrentItem == null) 297 | return; 298 | if (!CurrentItem.IsFolder) 299 | { 300 | if (Convert.ToUInt64(CurrentItem.Size) > 128 * 1024 * 1024) 301 | { 302 | MessageBox_Show(resultAction, Resources.TooLargeSize, Resources.Alert, ButtonEnum.Ok); 303 | return; 304 | } 305 | var copy = new ViewerViewModel(CurrentItem.FullName); 306 | await ShowViewerDialog.Handle(copy); 307 | resultAction?.Invoke(ButtonResult.Ok, null); 308 | } 309 | else 310 | { 311 | MessageBox_Show(resultAction, Resources.CantViewFolder, Resources.Alert, ButtonEnum.Ok); 312 | } 313 | } 314 | public void Edit() 315 | { 316 | Edit(null); 317 | } 318 | 319 | public void Edit(Action? resultAction) 320 | { 321 | if (CurrentItem == null) 322 | return; 323 | if (!CurrentItem.IsFolder) 324 | { 325 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 326 | { 327 | LaunchProcess("vi", CurrentItem.FullName); 328 | } 329 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 330 | { 331 | Process.Start("notepad.exe", CurrentItem.FullName); 332 | } 333 | resultAction?.Invoke(ButtonResult.Ok, null); 334 | } 335 | else 336 | { 337 | MessageBox_Show(resultAction, Resources.CantEditFolder, Resources.Alert, ButtonEnum.Ok); 338 | } 339 | } 340 | 341 | public void Zip() 342 | { 343 | _mainVM.Zip(); 344 | } 345 | 346 | public void Unzip() 347 | { 348 | _mainVM.Unzip(); 349 | } 350 | 351 | private void LaunchProcess(string program, string argument) 352 | { 353 | var process = new Process(); 354 | process.StartInfo.FileName = "x-terminal-emulator"; // Use the default terminal emulator 355 | process.StartInfo.Arguments = $"-e {program} \"{argument}\""; // Specify the command to run in the new terminal window 356 | process.StartInfo.UseShellExecute = false; // Required to use the terminal emulator 357 | process.Start(); 358 | } 359 | 360 | public void Delete(FileViewModel? item) 361 | { 362 | try 363 | { 364 | if (item == null) 365 | { 366 | return; 367 | } 368 | if (item.IsFolder) 369 | { 370 | Utils.DeleteDirectoryWithHiddenFiles(item.FullName); 371 | } 372 | else 373 | { 374 | File.Delete(item.FullName); 375 | } 376 | } 377 | catch 378 | { 379 | } 380 | } 381 | 382 | public void CreateNewFolder(string name) 383 | { 384 | string newFolder = Path.Combine(CurrentDirectory, name); 385 | if (Directory.Exists(newFolder)) 386 | { 387 | MessageBox_Show(null, Resources.FolderExists, Resources.Alert, ButtonEnum.Ok); 388 | return; 389 | } 390 | Directory.CreateDirectory(newFolder); 391 | } 392 | 393 | public void ProcessCurrentItem(bool goToParent = false) 394 | { 395 | if (CurrentItem == null) 396 | { 397 | return; 398 | } 399 | 400 | if (goToParent) 401 | { 402 | if (Directory.GetParent(CurrentDirectory) == null) 403 | { 404 | return; 405 | } 406 | 407 | var prevFolder = Path.GetFileName(CurrentDirectory); 408 | CurrentDirectory = Directory.GetParent(CurrentDirectory) != null ? Directory.GetParent(CurrentDirectory)!.FullName : CurrentDirectory; 409 | CurrentItem = FoldersFilesList.FirstOrDefault(f => f.Name == prevFolder); 410 | if (CurrentItem == null) 411 | CurrentItem = FoldersFilesList[0]; 412 | return; 413 | } 414 | 415 | if (CurrentItem.IsFolder) 416 | { 417 | if (CurrentItem.FullName == "..") 418 | { 419 | var prevFolder = Path.GetFileName(CurrentDirectory); 420 | CurrentDirectory = Directory.GetParent(CurrentDirectory) != null ? Directory.GetParent(CurrentDirectory)!.FullName : CurrentDirectory; 421 | CurrentItem = FoldersFilesList.FirstOrDefault(f => f.Name == prevFolder); 422 | if (CurrentItem == null) 423 | CurrentItem = FoldersFilesList[0]; 424 | } 425 | else 426 | { 427 | CurrentDirectory = CurrentItem.FullName; 428 | CurrentItem = FoldersFilesList[0]; 429 | } 430 | } 431 | else 432 | { 433 | // it is a file, open it 434 | new Process 435 | { 436 | StartInfo = new ProcessStartInfo(CurrentItem.FullName) 437 | { 438 | UseShellExecute = true 439 | } 440 | }.Start(); 441 | } 442 | } 443 | 444 | private void GetFilesFolders(string dir, IList filesFoldersList) 445 | { 446 | if (!Directory.Exists(dir) || !Path.IsPathFullyQualified(dir)) 447 | return; 448 | filesFoldersList.Clear(); 449 | _totalFolders = _totalFiles = 0; 450 | bool isParent = false; 451 | if (Directory.GetParent(CurrentDirectory) != null) 452 | { 453 | filesFoldersList.Add(new FileViewModel("..", true)); 454 | isParent = true; 455 | } 456 | 457 | if (OperatingSystem.IsWindows()) 458 | { 459 | FileInfo f = new FileInfo(CurrentDirectory); 460 | SelectedDrive = Path.GetPathRoot(f.FullName); 461 | } 462 | 463 | var options = new EnumerationOptions() 464 | { 465 | AttributesToSkip = OptionsModel.Instance.IsHiddenSystemFilesDisplayed ? 0 : FileAttributes.Hidden | FileAttributes.System, 466 | IgnoreInaccessible = true, 467 | RecurseSubdirectories = false, 468 | }; 469 | 470 | var subdirectoryEntries = Directory.EnumerateDirectories(dir, "*", options); 471 | var foldersList = new List(); 472 | foreach (string subdirectory in subdirectoryEntries) 473 | { 474 | try 475 | { 476 | foldersList.Add(new FileViewModel(subdirectory, true)); 477 | ++_totalFolders; 478 | } 479 | catch { } 480 | } 481 | 482 | var filesList = new List(); 483 | var fileEntries = Directory.EnumerateFiles(dir, "*", options); 484 | foreach (string fileName in fileEntries) 485 | { 486 | try 487 | { 488 | filesList.Add(new FileViewModel(fileName, false)); 489 | ++_totalFiles; 490 | } 491 | catch { } 492 | } 493 | if (Sorting == SortingBy.SortingByName) 494 | { 495 | if (Ascending) 496 | { 497 | foldersList = foldersList.OrderBy(entry => entry.Name).ToList(); 498 | filesList = filesList.OrderBy(entry => entry.Name).ToList(); 499 | } 500 | else 501 | { 502 | foldersList = foldersList.OrderByDescending(entry => entry.Name).ToList(); 503 | filesList = filesList.OrderByDescending(entry => entry.Name).ToList(); 504 | } 505 | } 506 | else if (Sorting == SortingBy.SortingByExt) 507 | { 508 | if (Ascending) 509 | { 510 | foldersList = foldersList.OrderBy(entry => entry.Extension).ToList(); 511 | filesList = filesList.OrderBy(entry => entry.Extension).ToList(); 512 | } 513 | else 514 | { 515 | foldersList = foldersList.OrderByDescending(entry => entry.Extension).ToList(); 516 | filesList = filesList.OrderByDescending(entry => entry.Extension).ToList(); 517 | } 518 | } 519 | else if (Sorting == SortingBy.SortingBySize) 520 | { 521 | if (Ascending) 522 | { 523 | foldersList = foldersList.OrderBy(entry => entry.Size).ToList(); 524 | filesList = filesList.OrderBy(entry => Convert.ToUInt64(entry.Size)).ToList(); 525 | } 526 | else 527 | { 528 | foldersList = foldersList.OrderByDescending(entry => entry.Size).ToList(); 529 | filesList = filesList.OrderByDescending(entry => Convert.ToUInt64(entry.Size)).ToList(); 530 | } 531 | } 532 | else if (Sorting == SortingBy.SortingByDate) 533 | { 534 | if (Ascending) 535 | { 536 | foldersList = foldersList.OrderBy(entry => entry.DateCreated).ToList(); 537 | filesList = filesList.OrderBy(entry => entry.DateCreated).ToList(); 538 | } 539 | else 540 | { 541 | foldersList = foldersList.OrderByDescending(entry => entry.DateCreated).ToList(); 542 | filesList = filesList.OrderByDescending(entry => entry.DateCreated).ToList(); 543 | } 544 | } 545 | 546 | foreach (var folder in foldersList) 547 | { 548 | filesFoldersList.Add(folder); 549 | } 550 | 551 | foreach (var file in filesList) 552 | { 553 | filesFoldersList.Add(file); 554 | } 555 | 556 | if (filesFoldersList.Count > 0) 557 | { 558 | CurrentItem = (isParent && filesFoldersList.Count > 1) ? 559 | filesFoldersList[1] : filesFoldersList[0]; 560 | } 561 | } 562 | 563 | public void NavigateToFileItem(string resultFilename) 564 | { 565 | var parent = Directory.GetParent(resultFilename); 566 | CurrentDirectory = parent!.FullName; 567 | CurrentItem = FoldersFilesList.First(f => f.FullName == resultFilename); 568 | RequestScroll(CurrentItem, null); 569 | } 570 | 571 | string? _selectedDrive; 572 | string? SelectedDrive 573 | { 574 | get { return _selectedDrive; } 575 | set 576 | { 577 | if (!Directory.Exists(value)) 578 | { 579 | MessageBox_Show(null, Resources.DriveNotAvailable, Resources.Alert, ButtonEnum.Ok); 580 | return; 581 | } 582 | 583 | FileInfo f = new FileInfo(CurrentDirectory); 584 | var driveFromDirectory = Path.GetPathRoot(f.FullName); 585 | 586 | _selectedDrive = value; 587 | if (_selectedDrive != driveFromDirectory) 588 | { 589 | CurrentDirectory = _selectedDrive; 590 | } 591 | this.RaisePropertyChanged(nameof(SelectedDrive)); 592 | } 593 | } 594 | } 595 | } 596 | --------------------------------------------------------------------------------