FlexLayout

main
Stepan Pilipenko 1 month ago
parent b486f16f4a
commit e4511340a9

@ -1,33 +1,43 @@
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace CommanderApp namespace CommanderApp;
public class FileSystemItem : INotifyPropertyChanged
{ {
public class FileSystemItem : INotifyPropertyChanged public string Name { get; set; } = string.Empty;
{ public string FullName { get; set; } = string.Empty;
public string Name { get; set; } public bool IsDirectory { get; set; }
public string FullName { get; set; }
public bool IsDirectory { get; set; }
public string Icon => IsDirectory ? "folder.png" : "file.png";
private bool _isSelected; public string DisplayText
public bool IsSelected {
get
{ {
get => _isSelected; if (string.IsNullOrWhiteSpace(Name))
set return "[No Name]";
{
_isSelected = value; if (Name == "..")
OnPropertyChanged(); return "⬆️ ..";
}
}
// Событие из INotifyPropertyChanged return IsDirectory ? $"📁 {Name}" : $"📄 {Name}";
public event PropertyChangedEventHandler? PropertyChanged; }
}
// Защищённый метод для вызова события private bool _isSelected;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 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 #if ANDROID || IOS || MACCATALYST
return FileSystem.Current.AppDataDirectory; return FileSystem.AppDataDirectory;
#else #else
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
// Убедимся, что путь корректен
return Path.GetFullPath(path);
#endif #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)) System.Diagnostics.Debug.WriteLine($"Directory does not exist: {path}");
return Enumerable.Empty<FileSystemItem>(); return Enumerable.Empty<FileSystemItem>();
}
var items = new List<FileSystemItem>(); var items = new List<FileSystemItem>();
// Добавляем ".." если не корень var parent = Directory.GetParent(path);
var parent = Directory.GetParent(path); if (parent != null)
if (parent != null) {
items.Add(new FileSystemItem
{ {
items.Add(new FileSystemItem Name = "..",
{ FullName = parent.FullName,
Name = "..", IsDirectory = true
FullName = parent.FullName, });
IsDirectory = true }
});
}
try
{
var dirs = Directory.GetDirectories(path) var dirs = Directory.GetDirectories(path)
.Select(d => new FileSystemItem .Select(d => new FileSystemItem
{ {
@ -48,8 +61,22 @@
items.AddRange(dirs.OrderBy(d => d.Name)); items.AddRange(dirs.OrderBy(d => d.Name));
items.AddRange(files.OrderBy(f => f.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" x:Class="CommanderApp.MainPage"
Title="MAUI Commander"> Title="MAUI Commander">
<!-- Изменяем RowDefinitions: добавляем строку под заголовком -->
<Grid RowDefinitions="Auto, Auto, *, Auto" ColumnDefinitions="*, *"> <Grid RowDefinitions="Auto, Auto, *, Auto" ColumnDefinitions="*, *">
<!-- Новая строка: пути над панелями (где были красные линии) --> <!-- Пути -->
<Label Text="{Binding LeftPath}" <Label Text="{Binding LeftPath}"
Grid.Row="1" Grid.Column="0" Grid.Row="1" Grid.Column="0"
Padding="12,6" Padding="12,6"
@ -28,50 +27,24 @@
LineBreakMode="MiddleTruncation" /> LineBreakMode="MiddleTruncation" />
<!-- Левая панель --> <!-- Левая панель -->
<CollectionView x:Name="LeftPanel" <ScrollView Grid.Row="2" Grid.Column="0">
Grid.Row="2" Grid.Column="0" <FlexLayout x:Name="LeftPanel"
ItemsSource="{Binding LeftItems}" Padding="0"
SelectionMode="None"> Direction="Column"
<CollectionView.ItemTemplate> JustifyContent="Start"
<DataTemplate> AlignItems="Start" />
<Grid Padding="10" BackgroundColor="Transparent"> </ScrollView>
<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>
<!-- Правая панель --> <!-- Правая панель -->
<CollectionView x:Name="RightPanel" <ScrollView Grid.Row="2" Grid.Column="1">
Grid.Row="2" Grid.Column="1" <FlexLayout x:Name="RightPanel"
ItemsSource="{Binding RightItems}" Padding="0"
SelectionMode="None"> Direction="Column"
<CollectionView.ItemTemplate> JustifyContent="Start"
<DataTemplate> AlignItems="Start" />
<Grid Padding="10" BackgroundColor="Transparent"> </ScrollView>
<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>
<!-- Адаптивный Toolbar (Footer) --> <!-- Toolbar -->
<FlexLayout Grid.Row="3" <FlexLayout Grid.Row="3"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Padding="10" Padding="10"

@ -1,5 +1,4 @@
// MainPage.xaml.cs using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
@ -10,15 +9,13 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
{ {
private readonly IFileSystemService _fileService; 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 _leftPath = string.Empty;
private string _rightPath = string.Empty; private string _rightPath = string.Empty;
private Grid? _currentSelectedGrid; // только один выделенный элемент в интерфейсе
public string LeftPath public string LeftPath
{ {
get => _leftPath; get => _leftPath;
@ -65,54 +62,94 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
private void LoadDirectory(string path, bool isLeft) private void LoadDirectory(string path, bool isLeft)
{ {
var items = _fileService.GetDirectoryContents(path).ToList(); 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) if (isLeft)
{ {
LeftItems.Clear();
foreach (var item in items) LeftItems.Add(item);
_currentLeftPath = path; _currentLeftPath = path;
LeftPath = path; // Теперь это вызовет обновление UI LeftPath = path;
} }
else else
{ {
RightItems.Clear();
foreach (var item in items) RightItems.Add(item);
_currentRightPath = path; _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) var button = (Button)sender;
{ var item = (FileSystemItem)button.CommandParameter;
// Сохраняем выбор
_selectedLeftItem = item;
// Если это ".." или папка — переходим _selectedLeftItem = item;
if (item.Name == ".." || item.IsDirectory) _selectedRightItem = null;
{
LoadDirectory(item.FullName, true); if (item.IsDirectory)
// После перехода — сбрасываем выбор, чтобы не осталось выделения в новой папке {
_selectedLeftItem = null; LoadDirectory(item.FullName, true);
LeftPanel.SelectedItem = null; _selectedLeftItem = null;
}
// Если это файл — ничего не делаем, оставляем его выделенным
} }
} }
private void OnRightSelectionChanged(object sender, SelectionChangedEventArgs e) private void OnRightItemActivated(object sender, EventArgs e)
{ {
if (e.CurrentSelection.FirstOrDefault() is FileSystemItem item) var button = (Button)sender;
{ var item = (FileSystemItem)button.CommandParameter;
_selectedRightItem = item;
if (item.Name == ".." || item.IsDirectory) _selectedRightItem = item;
{ _selectedLeftItem = null;
LoadDirectory(item.FullName, false);
_selectedRightItem = null; if (item.IsDirectory)
RightPanel.SelectedItem = null; {
} LoadDirectory(item.FullName, false);
_selectedRightItem = null;
} }
} }
@ -153,7 +190,6 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
else else
File.Delete(item.FullName); File.Delete(item.FullName);
// Обновляем обе панели (или только ту, откуда удалили)
LoadDirectory(_currentLeftPath, true); LoadDirectory(_currentLeftPath, true);
LoadDirectory(_currentRightPath, false); LoadDirectory(_currentRightPath, false);
} }
@ -164,7 +200,6 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
var result = await DisplayPromptAsync("New Folder", "Folder name:", "Create", "Cancel"); var result = await DisplayPromptAsync("New Folder", "Folder name:", "Create", "Cancel");
if (string.IsNullOrWhiteSpace(result)) return; if (string.IsNullOrWhiteSpace(result)) return;
// Создаём в активной панели (выберем левую, если выделение слева, иначе правую)
string targetPath = (_selectedLeftItem != null || LeftItems.Count > 0) ? _currentLeftPath : _currentRightPath; string targetPath = (_selectedLeftItem != null || LeftItems.Count > 0) ? _currentLeftPath : _currentRightPath;
string newPath = Path.Combine(targetPath, result.Trim()); string newPath = Path.Combine(targetPath, result.Trim());
@ -209,13 +244,11 @@ public partial class MainPage : ContentPage, INotifyPropertyChanged
return; return;
} }
// Определяем целевую директорию: противоположная панель
string destDir = (srcItem == _selectedLeftItem) ? _currentRightPath : _currentLeftPath; string destDir = (srcItem == _selectedLeftItem) ? _currentRightPath : _currentLeftPath;
try try
{ {
await operation(srcItem.FullName, destDir); await operation(srcItem.FullName, destDir);
// Обновляем обе панели
LoadDirectory(_currentLeftPath, true); LoadDirectory(_currentLeftPath, true);
LoadDirectory(_currentRightPath, false); 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; public new event PropertyChangedEventHandler? PropertyChanged;
protected virtual void NotifyPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void NotifyPropertyChanged([CallerMemberName] string? propertyName = null)

Loading…
Cancel
Save