FlexLayout

main
Stepan Pilipenko 4 weeks ago
parent b486f16f4a
commit e4511340a9

@ -1,33 +1,43 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace CommanderApp
namespace CommanderApp;
public class FileSystemItem : INotifyPropertyChanged
{
public class FileSystemItem : INotifyPropertyChanged
{
public string Name { get; set; }
public string FullName { get; set; }
public bool IsDirectory { get; set; }
public string Icon => IsDirectory ? "folder.png" : "file.png";
public string Name { get; set; } = string.Empty;
public string FullName { get; set; } = string.Empty;
public bool IsDirectory { get; set; }
private bool _isSelected;
public bool IsSelected
public string DisplayText
{
get
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged();
}
}
if (string.IsNullOrWhiteSpace(Name))
return "[No Name]";
if (Name == "..")
return "⬆️ ..";
// Событие из INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
return IsDirectory ? $"📁 {Name}" : $"📄 {Name}";
}
}
// Защищённый метод для вызова события
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
_isSelected = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

@ -1,35 +1,48 @@
namespace CommanderApp
using System.IO;
using System.Linq;
using Microsoft.Maui.Storage;
namespace CommanderApp;
public class FileSystemService : IFileSystemService
{
public class FileSystemService : IFileSystemService
public string GetRootPath()
{
public string GetRootPath()
{
#if ANDROID || IOS || MACCATALYST
return FileSystem.Current.AppDataDirectory;
return FileSystem.AppDataDirectory;
#else
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
// Убедимся, что путь корректен
return Path.GetFullPath(path);
#endif
}
}
public IEnumerable<FileSystemItem> GetDirectoryContents(string path)
{
// Нормализуем путь
path = Path.GetFullPath(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
public IEnumerable<FileSystemItem> GetDirectoryContents(string path)
if (!Directory.Exists(path))
{
if (!Directory.Exists(path))
return Enumerable.Empty<FileSystemItem>();
System.Diagnostics.Debug.WriteLine($"Directory does not exist: {path}");
return Enumerable.Empty<FileSystemItem>();
}
var items = new List<FileSystemItem>();
var items = new List<FileSystemItem>();
// Добавляем ".." если не корень
var parent = Directory.GetParent(path);
if (parent != null)
var parent = Directory.GetParent(path);
if (parent != null)
{
items.Add(new FileSystemItem
{
items.Add(new FileSystemItem
{
Name = "..",
FullName = parent.FullName,
IsDirectory = true
});
}
Name = "..",
FullName = parent.FullName,
IsDirectory = true
});
}
try
{
var dirs = Directory.GetDirectories(path)
.Select(d => new FileSystemItem
{
@ -48,8 +61,22 @@
items.AddRange(dirs.OrderBy(d => d.Name));
items.AddRange(files.OrderBy(f => f.Name));
return items;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error reading directory {path}: {ex.Message}");
// Возвращаем хотя бы ".." если возможно
if (items.Count == 0 && Directory.GetParent(path) != null)
{
items.Add(new FileSystemItem
{
Name = "..",
FullName = Directory.GetParent(path)!.FullName,
IsDirectory = true
});
}
}
return items;
}
}
}

@ -4,9 +4,8 @@
x:Class="CommanderApp.MainPage"
Title="MAUI Commander">
<!-- Изменяем RowDefinitions: добавляем строку под заголовком -->
<Grid RowDefinitions="Auto, Auto, *, Auto" ColumnDefinitions="*, *">
<!-- Новая строка: пути над панелями (где были красные линии) -->
<!-- Пути -->
<Label Text="{Binding LeftPath}"
Grid.Row="1" Grid.Column="0"
Padding="12,6"
@ -28,50 +27,24 @@
LineBreakMode="MiddleTruncation" />
<!-- Левая панель -->
<CollectionView x:Name="LeftPanel"
Grid.Row="2" Grid.Column="0"
ItemsSource="{Binding LeftItems}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10" BackgroundColor="Transparent">
<HorizontalStackLayout Spacing="10">
<Image Source="{Binding Icon}" WidthRequest="20" HeightRequest="20" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
<Grid.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Tapped="OnLeftItemTapped" />
<TapGestureRecognizer NumberOfTapsRequired="2" Tapped="OnLeftItemDoubleTapped" />
</Grid.GestureRecognizers>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ScrollView Grid.Row="2" Grid.Column="0">
<FlexLayout x:Name="LeftPanel"
Padding="0"
Direction="Column"
JustifyContent="Start"
AlignItems="Start" />
</ScrollView>
<!-- Правая панель -->
<CollectionView x:Name="RightPanel"
Grid.Row="2" Grid.Column="1"
ItemsSource="{Binding RightItems}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="10" BackgroundColor="Transparent">
<HorizontalStackLayout Spacing="10">
<Image Source="{Binding Icon}" WidthRequest="20" HeightRequest="20" />
<Label Text="{Binding Name}" VerticalOptions="Center" />
</HorizontalStackLayout>
<Grid.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Tapped="OnRightItemTapped" />
<TapGestureRecognizer NumberOfTapsRequired="2" Tapped="OnRightItemDoubleTapped" />
</Grid.GestureRecognizers>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<ScrollView Grid.Row="2" Grid.Column="1">
<FlexLayout x:Name="RightPanel"
Padding="0"
Direction="Column"
JustifyContent="Start"
AlignItems="Start" />
</ScrollView>
<!-- Адаптивный Toolbar (Footer) -->
<!-- Toolbar -->
<FlexLayout Grid.Row="3"
Grid.ColumnSpan="2"
Padding="10"

@ -1,5 +1,4 @@
// MainPage.xaml.cs
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls;
@ -10,15 +9,13 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
{
private readonly IFileSystemService _fileService;
public ObservableCollection<FileSystemItem> LeftItems { get; set; } = new();
public ObservableCollection<FileSystemItem> RightItems { get; set; } = new();
// Коллекции больше не нужны для привязки, но оставим для логики
public ObservableCollection<FileSystemItem> LeftItems { get; } = new();
public ObservableCollection<FileSystemItem> RightItems { get; } = new();
// --- Изменено: приватные поля + INotifyPropertyChanged ---
private string _leftPath = string.Empty;
private string _rightPath = string.Empty;
private Grid? _currentSelectedGrid; // только один выделенный элемент в интерфейсе
public string LeftPath
{
get => _leftPath;
@ -65,54 +62,94 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
private void LoadDirectory(string path, bool isLeft)
{
var items = _fileService.GetDirectoryContents(path).ToList();
var panel = isLeft ? LeftPanel : RightPanel;
var targetItems = isLeft ? LeftItems : RightItems;
var currentPathProperty = isLeft ? nameof(_currentLeftPath) : nameof(_currentRightPath);
// Очистка панели
panel.Clear();
targetItems.Clear();
// Заполнение
foreach (var item in items)
{
targetItems.Add(item);
var button = new Button
{
Text = item.DisplayText,
TextColor = Colors.Black,
Padding = new Thickness(10),
HeightRequest = 40,
BackgroundColor = Colors.Transparent,
BorderWidth = 0,
CommandParameter = item
};
button.Clicked += isLeft ? OnLeftItemActivated : OnRightItemActivated;
// Добавляем визуальные состояния
var normal = new VisualState { Name = "Normal" };
var focused = new VisualState { Name = "Focused" };
focused.Setters.Add(new Setter { Property = Button.BorderColorProperty, Value = Colors.Black });
focused.Setters.Add(new Setter { Property = Button.BorderWidthProperty, Value = 2 });
var pressed = new VisualState { Name = "Pressed" };
pressed.Setters.Add(new Setter { Property = Button.BackgroundColorProperty, Value = Color.FromArgb("#e0e0e0") });
var commonStates = new VisualStateGroup { Name = "CommonStates" };
commonStates.States.Add(normal);
commonStates.States.Add(focused);
commonStates.States.Add(pressed);
var groups = new VisualStateGroupList();
groups.Add(commonStates);
VisualStateManager.SetVisualStateGroups(button, groups);
panel.Add(button);
}
// Обновление пути
if (isLeft)
{
LeftItems.Clear();
foreach (var item in items) LeftItems.Add(item);
_currentLeftPath = path;
LeftPath = path; // Теперь это вызовет обновление UI
LeftPath = path;
}
else
{
RightItems.Clear();
foreach (var item in items) RightItems.Add(item);
_currentRightPath = path;
RightPath = path; // И это тоже
RightPath = path;
}
}
private void OnLeftSelectionChanged(object sender, SelectionChangedEventArgs e)
private void OnLeftItemActivated(object sender, EventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is FileSystemItem item)
{
// Сохраняем выбор
_selectedLeftItem = item;
var button = (Button)sender;
var item = (FileSystemItem)button.CommandParameter;
// Если это ".." или папка — переходим
if (item.Name == ".." || item.IsDirectory)
{
LoadDirectory(item.FullName, true);
// После перехода — сбрасываем выбор, чтобы не осталось выделения в новой папке
_selectedLeftItem = null;
LeftPanel.SelectedItem = null;
}
// Если это файл — ничего не делаем, оставляем его выделенным
_selectedLeftItem = item;
_selectedRightItem = null;
if (item.IsDirectory)
{
LoadDirectory(item.FullName, true);
_selectedLeftItem = null;
}
}
private void OnRightSelectionChanged(object sender, SelectionChangedEventArgs e)
private void OnRightItemActivated(object sender, EventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is FileSystemItem item)
{
_selectedRightItem = item;
var button = (Button)sender;
var item = (FileSystemItem)button.CommandParameter;
if (item.Name == ".." || item.IsDirectory)
{
LoadDirectory(item.FullName, false);
_selectedRightItem = null;
RightPanel.SelectedItem = null;
}
_selectedRightItem = item;
_selectedLeftItem = null;
if (item.IsDirectory)
{
LoadDirectory(item.FullName, false);
_selectedRightItem = null;
}
}
@ -153,7 +190,6 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
else
File.Delete(item.FullName);
// Обновляем обе панели (или только ту, откуда удалили)
LoadDirectory(_currentLeftPath, true);
LoadDirectory(_currentRightPath, false);
}
@ -164,7 +200,6 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
var result = await DisplayPromptAsync("New Folder", "Folder name:", "Create", "Cancel");
if (string.IsNullOrWhiteSpace(result)) return;
// Создаём в активной панели (выберем левую, если выделение слева, иначе правую)
string targetPath = (_selectedLeftItem != null || LeftItems.Count > 0) ? _currentLeftPath : _currentRightPath;
string newPath = Path.Combine(targetPath, result.Trim());
@ -209,13 +244,11 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
return;
}
// Определяем целевую директорию: противоположная панель
string destDir = (srcItem == _selectedLeftItem) ? _currentRightPath : _currentLeftPath;
try
{
await operation(srcItem.FullName, destDir);
// Обновляем обе панели
LoadDirectory(_currentLeftPath, true);
LoadDirectory(_currentRightPath, false);
}
@ -225,81 +258,6 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
}
}
private void OnLeftItemTapped(object sender, TappedEventArgs e)
{
var grid = (Grid)sender;
var item = (FileSystemItem)grid.BindingContext;
// Сбрасываем выделение у предыдущего элемента (где бы он ни был)
if (_currentSelectedGrid != null)
{
_currentSelectedGrid.BackgroundColor = Colors.Transparent;
}
// Выделяем новый элемент
grid.BackgroundColor = Color.FromArgb("#d0e0ff");
_currentSelectedGrid = grid;
// Обновляем активный выбор
_selectedLeftItem = item;
_selectedRightItem = null; // сбрасываем выбор в правой панели
}
private void OnLeftItemDoubleTapped(object sender, TappedEventArgs e)
{
var grid = (Grid)sender;
var item = (FileSystemItem)grid.BindingContext;
if (item.IsDirectory)
{
LoadDirectory(item.FullName, isLeft: true);
// Сбрасываем выделение, так как панель обновляется
if (_currentSelectedGrid != null)
{
_currentSelectedGrid.BackgroundColor = Colors.Transparent;
_currentSelectedGrid = null;
}
_selectedLeftItem = null;
_selectedRightItem = null;
}
}
private void OnRightItemTapped(object sender, TappedEventArgs e)
{
var grid = (Grid)sender;
var item = (FileSystemItem)grid.BindingContext;
if (_currentSelectedGrid != null)
{
_currentSelectedGrid.BackgroundColor = Colors.Transparent;
}
grid.BackgroundColor = Color.FromArgb("#d0e0ff");
_currentSelectedGrid = grid;
_selectedRightItem = item;
_selectedLeftItem = null; // сбрасываем выбор в левой панели
}
private void OnRightItemDoubleTapped(object sender, TappedEventArgs e)
{
var grid = (Grid)sender;
var item = (FileSystemItem)grid.BindingContext;
if (item.IsDirectory)
{
LoadDirectory(item.FullName, isLeft: false);
if (_currentSelectedGrid != null)
{
_currentSelectedGrid.BackgroundColor = Colors.Transparent;
_currentSelectedGrid = null;
}
_selectedLeftItem = null;
_selectedRightItem = null;
}
}
// --- Реализация INotifyPropertyChanged ---
public new event PropertyChangedEventHandler? PropertyChanged;
protected virtual void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)

Loading…
Cancel
Save