diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..d78f895 --- /dev/null +++ b/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..7661d7d --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,37 @@ + + +using NotesDataAnalyst.NoteContent; + +namespace NotesDataAnalyst +{ + public partial class App : Application + { + public static List AllNotes { get; set; } = new List(); + + public static DatabaseContext Database { get; private set; } + + public App() + { + InitializeComponent(); + string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "notes.db"); + Database = new DatabaseContext(dbPath); + MainPage = new AppShell(); + } + + // Метод длÑ� добавлениÑ� или Ñ�охранениÑ� заметки + public static void SaveNote(Note note) + { + var existingNote = AllNotes.FirstOrDefault(n => n.Id == note.Id); + if (existingNote == null) + { + AllNotes.Add(note); + } + else + { + // ОбновлÑ�ем Ñ�ущеÑ�твующую заметку + existingNote.Title = note.Title; + existingNote.Text = note.Text; + } + } + } +} diff --git a/AppShell.xaml b/AppShell.xaml new file mode 100644 index 0000000..96d0e53 --- /dev/null +++ b/AppShell.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/AppShell.xaml.cs b/AppShell.xaml.cs new file mode 100644 index 0000000..4209173 --- /dev/null +++ b/AppShell.xaml.cs @@ -0,0 +1,74 @@ +namespace NotesDataAnalyst +{ + public partial class AppShell : Shell + { + public AppShell() + { + InitializeComponent(); + CategoryManager.LoadCategories(); // Загружаем категории при запуÑ�ке + // РегиÑ�трациÑ� маршрута длÑ� MainPage + Routing.RegisterRoute("MainPage", typeof(MainPage)); + } + + private async void OnCategoriesMenuClicked(object sender, EventArgs e) + { + // Открываем вÑ�плывающее окно Ñ� возможноÑ�тью управлениÑ� категориÑ�ми + string action = await DisplayActionSheet("Управление категориÑ�ми", "Отмена", null, + "Выбрать категорию", "Добавить категорию", "Удалить категорию"); + + switch (action) + { + case "Выбрать категорию": + await SelectCategory(); + break; + case "Добавить категорию": + await AddCategory(); + break; + case "Удалить категорию": + await DeleteCategory(); + break; + } + } + + private async Task SelectCategory() + { + // Ð’Ñ�плывающее окно длÑ� выбора категории + string selectedCategory = await DisplayActionSheet("Выберите категорию", "Отмена", null, CategoryManager.Categories.ToArray()); + if (!string.IsNullOrEmpty(selectedCategory) && selectedCategory != "Отмена") + { + // УÑ�танавливаем выбранную категорию + CategoryManager.ActiveCategory = selectedCategory; + // Переход к MainPage отноÑ�ительно текущего Ñ�тека, без абÑ�олютного маршрута + await Shell.Current.GoToAsync("MainPage"); + } + } + + private async Task AddCategory() + { + // Ð’Ñ�плывающее окно длÑ� ввода названиÑ� новой категории + string newCategory = await DisplayPromptAsync("Ð�оваÑ� категориÑ�", "Введите название категории:"); + if (!string.IsNullOrWhiteSpace(newCategory) && !CategoryManager.Categories.Contains(newCategory)) + { + CategoryManager.AddCategory(newCategory); + } + } + + private async Task DeleteCategory() + { + // Ð’Ñ�плывающее окно длÑ� выбора категории длÑ� удалениÑ� + string categoryToDelete = await DisplayActionSheet("Удалить категорию", "Отмена", null, CategoryManager.Categories.ToArray()); + if (!string.IsNullOrEmpty(categoryToDelete) && categoryToDelete != "Отмена") + { + // УдалÑ�ем категорию + CategoryManager.Categories.Remove(categoryToDelete); + + // СброÑ� активной категории, еÑ�ли она была удалена + if (CategoryManager.ActiveCategory == categoryToDelete) + { + CategoryManager.ActiveCategory = "Ð’Ñ�е категории"; + } + } + } + } + +} diff --git a/GraphDrawable.cs b/GraphDrawable.cs new file mode 100644 index 0000000..e715d49 --- /dev/null +++ b/GraphDrawable.cs @@ -0,0 +1,80 @@ +using Microsoft.Maui.Graphics; +using System.Collections.Generic; + +namespace NotesDataAnalyst +{ + public class GraphDrawable : IDrawable + { + private readonly List _nodes; + private readonly List _connections; + + public GraphDrawable(List nodes, List connections) + { + _nodes = nodes; + _connections = connections; + } + + private void DrawConnection(ICanvas canvas, GraphConnection connection) + { + var startX = connection.FromNode.Position.X + connection.FromNode.Size.Width / 2; + var startY = connection.FromNode.Position.Y + connection.FromNode.Size.Height / 2; + var endX = connection.ToNode.Position.X + connection.ToNode.Size.Width / 2; + var endY = connection.ToNode.Position.Y + connection.ToNode.Size.Height / 2; + + canvas.StrokeColor = Colors.Gray; + canvas.StrokeSize = 2; + canvas.DrawLine((float)startX, (float)startY, (float)endX, (float)endY); + } + + + private void DrawNode(ICanvas canvas, GraphNode node) + { + canvas.FillColor = node.BackgroundColor; + canvas.FillRectangle((float)node.Position.X, (float)node.Position.Y, (float)node.Size.Width, (float)node.Size.Height); + + canvas.FontColor = Colors.Black; + canvas.DrawString(node.Text, (float)node.Position.X + 10, (float)node.Position.Y + 20, HorizontalAlignment.Left); + } + + public void Draw(ICanvas canvas, RectF dirtyRect) + { + canvas.StrokeColor = Colors.Black; + canvas.StrokeSize = 2; + + // ОтриÑ�овка Ñ�вÑ�зей + foreach (var connection in _connections) + { + canvas.DrawLine( + (float)connection.FromNode.Position.X, (float)connection.FromNode.Position.Y, + (float)connection.ToNode.Position.X, (float)connection.ToNode.Position.Y); + } + + // ОтриÑ�овка узлов + foreach (var node in _nodes) + { + // Заливка фона узла + canvas.FillColor = node.BackgroundColor; + canvas.FillRectangle((float)node.Position.X, (float)node.Position.Y, (float)node.Size.Width, (float)node.Size.Height); + + // ОтриÑ�овка текÑ�та + canvas.FontColor = Colors.Black; + canvas.DrawString(node.Text, (float)node.Position.X + 5, (float)node.Position.Y + 5, HorizontalAlignment.Left); + + // ЕÑ�ли нода выбрана, риÑ�уем пунктирную обводку и "ручку" длÑ� изменениÑ� размера + if (node.IsActive) + { + canvas.StrokeColor = Colors.Blue; + canvas.StrokeDashPattern = new float[] { 4, 2 }; + canvas.DrawRectangle((float)node.Position.X, (float)node.Position.Y, (float)node.Size.Width, (float)node.Size.Height); + + // Ручка длÑ� изменениÑ� размера + canvas.FillColor = Colors.Gray; + canvas.FillRectangle( + (float)(node.Position.X + node.Size.Width - 10), + (float)(node.Position.Y + node.Size.Height - 10), + 10, 10); + } + } + } + } +} diff --git a/GraphNode.cs b/GraphNode.cs new file mode 100644 index 0000000..fa4a11d --- /dev/null +++ b/GraphNode.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NotesDataAnalyst +{ + public class GraphNode + { + public bool IsActive { get; set; } + public string Id { get; } = Guid.NewGuid().ToString(); // Уникальный идентификатор + public string Text { get; set; } + public Color BackgroundColor { get; set; } = Colors.LightBlue; + public Point Position { get; set; } // ПозициÑ� узла + public Size Size { get; set; } = new Size(100, 50); // Размер узла + public List Children { get; } = new List(); // СпиÑ�ок дочерних узлов + } + + public class GraphConnection + { + public GraphNode FromNode { get; set; } // Узел начала Ñ�вÑ�зи + public GraphNode ToNode { get; set; } // Узел конца Ñ�вÑ�зи + } +} diff --git a/GraphPage.xaml b/GraphPage.xaml new file mode 100644 index 0000000..79fb8ce --- /dev/null +++ b/GraphPage.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/GraphPage.xaml.cs b/GraphPage.xaml.cs new file mode 100644 index 0000000..c3f9f78 --- /dev/null +++ b/GraphPage.xaml.cs @@ -0,0 +1,310 @@ +using System.Diagnostics; +using System.Linq; + +namespace NotesDataAnalyst +{ + + public partial class GraphPage : ContentPage + { + private readonly Entry _textEditor; // Ïîëå äëÿ ââîäà òåêñòà + private PointF _initialNodePosition; // Õðàíèò íà÷àëüíóþ ïîçèöèþ íîäû + private PointF _initialTouchPosition; // Õðàíèò íà÷àëüíóþ ïîçèöèþ êàñàíèÿ + private SizeF _initialNodeSize; // Õðàíèò íà÷àëüíûé ðàçìåð íîäû + + private readonly List _nodes = new(); + private readonly List _connections = new(); + private GraphNode _selectedNode; + private bool _isResizing = false; + + public GraphPage() + { + InitializeComponent(); + + // Ñîçäàåì ñêðûòîå ïîëå äëÿ ðåäàêòèðîâàíèÿ òåêñòà + _textEditor = new Entry + { + IsVisible = false, + BackgroundColor = Colors.Transparent, + FontSize = 18, + TextColor = Colors.Black + }; + _textEditor.Completed += OnTextEditorCompleted; // Ñîõðàíÿåì òåêñò ïðè çàâåðøåíèè ââîäà + this.Add(_textEditor); + + // Íàñòðàèâàåì Drawable äëÿ îòðèñîâêè óçëîâ è ñâÿçåé + var graphDrawable = new GraphDrawable(_nodes, _connections); + canvasView.Drawable = graphDrawable; + + // Äîáàâëÿåì æåñòû + var panGesture = new PanGestureRecognizer(); + panGesture.PanUpdated += OnPanUpdated; + canvasView.GestureRecognizers.Add(panGesture); + + var tapGesture = new TapGestureRecognizer(); + tapGesture.Tapped += OnCanvasTapped; + canvasView.GestureRecognizers.Add(tapGesture); + } + + private void OnNodeTapped(object sender, TappedEventArgs e) + { + var node = _selectedNode; + + var tapPosition = e.GetPosition((View)sender); + if (tapPosition.HasValue) + { + var touchPoint = new PointF((float)tapPosition.Value.X, (float)tapPosition.Value.Y); + _selectedNode = _nodes.FirstOrDefault(node => IsPointInsideNode(touchPoint, node)); + + if (_selectedNode != null) + { + // Ïîçèöèîíèðóåì è ïîêàçûâàåì òåêñòîâîå ïîëå + _textEditor.Text = _selectedNode.Text; + _textEditor.IsVisible = true; + _textEditor.Focus(); + AbsoluteLayout.SetLayoutBounds(_textEditor, new Rect(_selectedNode.Position.X + 5, _selectedNode.Position.Y + 5, _selectedNode.Size.Width - 10, 30)); + } + } + + Debug.WriteLine("Íàæàòà íîäà - {node}"); + } + + private void OnTextEditorCompleted(object sender, EventArgs e) + { + // Ñîõðàíÿåì òåêñò â íîäó + if (_selectedNode != null) + { + _selectedNode.Text = _textEditor.Text; + _textEditor.IsVisible = false; + canvasView.Invalidate(); + } + } + + + + private void OnChildClicked(object sender, EventArgs e) + { + if (_selectedNode != null) + { + var child = new GraphNode + { + Text = "Child Node", + Position = new Point(_selectedNode.Position.X + 5, _selectedNode.Position.Y + 5), + Size = _selectedNode.Size, + BackgroundColor = Colors.Gray, + }; + _nodes.Add(child); + _connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = child }); + canvasView.Invalidate(); + + } + else { Debug.WriteLine("Ñíà÷àëà âûáèðèòå íîäó"); } + } + + private async void OnAddNodeClicked(object sender, EventArgs e) + { + + var newNode = new GraphNode + { + Text = "Íîâàÿ íîäà", + Position = new Point(300, 200), + Size = new Size(200, 200), + + }; + + _nodes.Add(newNode); + canvasView.Invalidate(); + } + + // Îáðàáîò÷èê äëÿ âûáîðà óçëà + // Îáðàáîò÷èê äëÿ âûáîðà óçëà + private void OnCanvasTapped(object sender, TappedEventArgs e) + { + var tapPosition = e.GetPosition((View)sender); + _selectedNode = null; + + if (tapPosition.HasValue) + { + var touchPoint = new PointF((float)tapPosition.Value.X, (float)tapPosition.Value.Y); + _selectedNode = _nodes.FirstOrDefault(node => IsPointInsideNode(touchPoint, node)); + + // Ñáðîñ àêòèâíîñòè äëÿ âñåõ óçëîâ, êðîìå âûáðàííîãî + foreach (var node in _nodes) + node.IsActive = node == _selectedNode; + + // Îáíîâëÿåì ýêðàí ïîñëå èçìåíåíèÿ ñîñòîÿíèÿ óçëîâ + canvasView.Invalidate(); + } + } + + + private void OnPanUpdated(object sender, PanUpdatedEventArgs e) + { + if (_selectedNode == null) return; + + if (e.StatusType == GestureStatus.Started) + { + // Ñîõðàíÿåì íà÷àëüíóþ ïîçèöèþ íîäû è ðàçìåð + _initialNodePosition = _selectedNode.Position; + _initialTouchPosition = new PointF((float)e.TotalX, (float)e.TotalY); + _initialNodeSize = _selectedNode.Size; // Ñîõðàíÿåì íà÷àëüíûé ðàçìåð + + // Ïðîâåðÿåì, íàõîäèìñÿ ëè ìû íà ðó÷êå èçìåíåíèÿ ðàçìåðà + _isResizing = IsOnRightResizeHandle(_selectedNode, _initialTouchPosition) || + IsOnLeftResizeHandle(_selectedNode, _initialTouchPosition) || + IsOnTopResizeHandle(_selectedNode, _initialTouchPosition) || + IsOnBottomResizeHandle(_selectedNode, _initialTouchPosition); + + } + else if (e.StatusType == GestureStatus.Running) + { + // Âû÷èñëÿåì ñìåùåíèå îò íà÷àëüíîé ïîçèöèè êàñàíèÿ + var deltaX = (float)e.TotalX - _initialTouchPosition.X; + var deltaY = (float)e.TotalY - _initialTouchPosition.Y; + + if (_isResizing) + { + ResizeNode(_selectedNode, deltaX, deltaY); + } + else + { + // Ïåðåìåùåíèå + _selectedNode.Position = new PointF( + _initialNodePosition.X + deltaX, + _initialNodePosition.Y + deltaY + ); + } + + canvasView.Invalidate(); + } + else if (e.StatusType == GestureStatus.Completed) + { + _isResizing = false; + } + } + + // Èçìåíåíèå ðàçìåðà óçëà + private void ResizeNode(GraphNode node, double deltaX, double deltaY) + { + const float minSize = 50f; // Óáèðàåì "f" äëÿ double + + // Óâåëè÷èâàåì èëè óìåíüøàåì ðàçìåð â çàâèñèìîñòè îò íàïðàâëåíèÿ + if (_isResizing) + { + // Åñëè ìû òÿíåì ïðàâóþ "ðó÷êó", óâåëè÷èâàåì øèðèíó + if (IsOnRightResizeHandle(node, _initialTouchPosition)) + { + _selectedNode.Size = new Size( + Math.Max(minSize, node.Size.Width + deltaX), // double + _selectedNode.Size.Height // double + ); + } + // Åñëè ìû òÿíåì ëåâóþ "ðó÷êó", óìåíüøàåì øèðèíó + else if (IsOnLeftResizeHandle(node, _initialTouchPosition)) + { + _selectedNode.Size = new Size( + Math.Max(minSize, _selectedNode.Size.Width - deltaX), // double + _selectedNode.Size.Height // double + ); + + // Ïåðåìåùàåì íîäó âëåâî, ÷òîáû ñîõðàíèòü åå ïîçèöèþ + _selectedNode.Position = new Point(_selectedNode.Position.X + deltaX, _selectedNode.Position.Y); // double + } + // Åñëè ìû òÿíåì âåðõíþþ "ðó÷êó", óìåíüøàåì âûñîòó + else if (IsOnTopResizeHandle(node, _initialTouchPosition)) + { + _selectedNode.Size = new Size( + _selectedNode.Size.Width, // double + Math.Max(minSize, _selectedNode.Size.Height - deltaY) // double + ); + + // Ïåðåìåùàåì íîäó ââåðõ, ÷òîáû ñîõðàíèòü åå ïîçèöèþ + _selectedNode.Position = new Point(_selectedNode.Position.X, _selectedNode.Position.Y + deltaY); // double + } + // Åñëè ìû òÿíåì íèæíþþ "ðó÷êó", óâåëè÷èâàåì âûñîòó + else if (IsOnBottomResizeHandle(node, _initialTouchPosition)) + { + _selectedNode.Size = new Size( + _selectedNode.Size.Width, // double + Math.Max(minSize, _selectedNode.Size.Height + deltaY) // double + ); + } + } + + + + + } + + + private bool IsOnRightResizeHandle(GraphNode node, PointF point) + { + const float handleSize = 400; + return Math.Abs(point.X - (node.Position.X + node.Size.Width)) <= handleSize; + } + + private bool IsOnLeftResizeHandle(GraphNode node, PointF point) + { + const float handleSize = 400; + return Math.Abs(point.X - node.Position.X) <= handleSize; + } + + private bool IsOnTopResizeHandle(GraphNode node, PointF point) + { + const float handleSize = 400; + return Math.Abs(point.Y - node.Position.Y) <= handleSize; + } + + private bool IsOnBottomResizeHandle(GraphNode node, PointF point) + { + const float handleSize = 400; + return Math.Abs(point.Y - (node.Position.Y + node.Size.Height)) <= handleSize; + } + + + // Ìåòîä äëÿ ïðîâåðêè, íàõîäèòñÿ ëè òî÷êà âíóòðè óçëà + private bool IsPointInsideNode(PointF point, GraphNode node) + { + return point.X >= node.Position.X && + point.X <= node.Position.X + node.Size.Width && + point.Y >= node.Position.Y && + point.Y <= node.Position.Y + node.Size.Height; + } + + + // Ïðîâåðêà, íàõîäèòñÿ ëè òî÷êà íà "ðó÷êå" èçìåíåíèÿ ðàçìåðà + private bool IsOnResizeHandle(GraphNode node, PointF point) + { + const float handleSize = 200f; // Ðàçìåð ðó÷êè èçìåíåíèÿ ðàçìåðà + + // Îïðåäåëÿåì ïðÿìîóãîëüíèêè äëÿ êàæäîé ðó÷êè + var rightHandleRect = new RectF( + (float)(node.Position.X + node.Size.Width - handleSize), + (float)(node.Position.Y + node.Size.Height - handleSize), + handleSize, handleSize); + + var leftHandleRect = new RectF( + (float)(node.Position.X), + (float)(node.Position.Y + node.Size.Height - handleSize), + handleSize, handleSize); + + var topHandleRect = new RectF( + (float)(node.Position.X + (node.Size.Width / 2) - (handleSize / 2)), + (float)(node.Position.Y), + handleSize, handleSize); + + var bottomHandleRect = new RectF( + (float)(node.Position.X + (node.Size.Width / 2) - (handleSize / 2)), + (float)(node.Position.Y + node.Size.Height - handleSize), + handleSize, handleSize); + + // Ïðîâåðÿåì, íàõîäèòñÿ ëè êóðñîð íà îäíîé èç ðó÷åê + return rightHandleRect.Contains(point) || + leftHandleRect.Contains(point) || + topHandleRect.Contains(point) || + bottomHandleRect.Contains(point); + } + + + } +} + diff --git a/MauiProgram.cs b/MauiProgram.cs new file mode 100644 index 0000000..63569a0 --- /dev/null +++ b/MauiProgram.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; +using CommunityToolkit.Maui; + +namespace NotesDataAnalyst +{ + public static class MauiProgram + { + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .UseMauiCommunityToolkit() // Подключение CommunityToolkit.Maui + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + + return builder.Build(); + } + } +} diff --git a/NoteContent/CategoryManager.cs b/NoteContent/CategoryManager.cs new file mode 100644 index 0000000..00cfbff --- /dev/null +++ b/NoteContent/CategoryManager.cs @@ -0,0 +1,48 @@ +using System.Collections.ObjectModel; +using Microsoft.Maui.Storage; + +public static class CategoryManager +{ + public static ObservableCollection Categories { get; private set; } = new ObservableCollection(); + + private const string ActiveCategoryKey = "ActiveCategory"; + + public static string ActiveCategory + { + get => Preferences.Get(ActiveCategoryKey, Categories.FirstOrDefault() ?? ""); // Проверка на null + set + { + if (!string.IsNullOrEmpty(value)) + { + Preferences.Set(ActiveCategoryKey, value); + } + } + } + + public static void LoadCategories() + { + if (Categories.Count == 0) // Проверка, чтобы не загружать повторно + { + Categories.Add("Личное"); + Categories.Add("Работа"); + Categories.Add("Дом"); + + // УÑ�танавливаем первую категорию как активную, еÑ�ли она не задана + if (string.IsNullOrEmpty(ActiveCategory)) + { + ActiveCategory = Categories.First(); + } + } + } + + public static void AddCategory(string category) + { + if (!Categories.Contains(category)) + { + Categories.Add(category); + ActiveCategory = category; // Сразу делаем новую категорию активной + } + } +} + + diff --git a/NoteContent/DatabaseContext.cs b/NoteContent/DatabaseContext.cs new file mode 100644 index 0000000..eef0e13 --- /dev/null +++ b/NoteContent/DatabaseContext.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using System.Collections.ObjectModel; +namespace NotesDataAnalyst.NoteContent +{ + + + public class DatabaseContext + { + private readonly string _connectionString; + + public DatabaseContext(string dbPath) + { + _connectionString = $"Data Source={dbPath}"; + InitializeDatabase(); + } + + private void InitializeDatabase() + { + using (var connection = new SqliteConnection(_connectionString)) + { + connection.Open(); + + var command = connection.CreateCommand(); + command.CommandText = + @" + CREATE TABLE IF NOT EXISTS Notes ( + Id TEXT PRIMARY KEY, + Category TEXT, + Title TEXT, + Image TEXT, + Text TEXT, + DateCreated TEXT + ); + "; + command.ExecuteNonQuery(); + } + } + + public ObservableCollection GetAllNotes() + { + var notes = new ObservableCollection(); + + using (var connection = new SqliteConnection(_connectionString)) + { + connection.Open(); + + var command = connection.CreateCommand(); + command.CommandText = "SELECT * FROM Notes"; + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + var note = new Note + { + Id = reader.GetString(0), + Category = reader.GetString(1), + Title = reader.GetString(2), + Image = reader.GetString(3), + Text = reader.GetString(4), // Загружаем текÑ�Ñ‚ из БД + DateCreated = DateTime.TryParse(reader.GetString(5), out var date) ? date : DateTime.UtcNow + }; + + notes.Add(note); + } + } + } + + return notes; + } + + public void SaveNote(Note note) + { + using (var connection = new SqliteConnection(_connectionString)) + { + connection.Open(); + + var command = connection.CreateCommand(); + command.CommandText = + @" + INSERT OR REPLACE INTO Notes (Id, Category, Title, Image, Text, DateCreated) + VALUES ($id, $category, $title, $image, $text, $dateCreated) + "; + command.Parameters.AddWithValue("$id", note.Id); + command.Parameters.AddWithValue("$category", note.Category); + command.Parameters.AddWithValue("$title", note.Title); + command.Parameters.AddWithValue("$image", note.Image); + command.Parameters.AddWithValue("$text", note.Text); + command.Parameters.AddWithValue("$dateCreated", note.DateCreated.ToString("o")); // СохранÑ�ем в формате ISO + command.ExecuteNonQuery(); + + } + } + + public void DeleteNote(Note note) + { + if (string.IsNullOrEmpty(note.Id)) + { + Console.WriteLine("Ошибка: У заметки отÑ�утÑ�твует Id."); + return; + } + + using (var connection = new SqliteConnection(_connectionString)) + { + connection.Open(); + + var command = connection.CreateCommand(); + command.CommandText = + @" + DELETE FROM Notes + WHERE Id = $id + "; + command.Parameters.AddWithValue("$id", note.Id); + command.ExecuteNonQuery(); + int result = command.ExecuteNonQuery(); + + Console.WriteLine(result > 0 ? "Заметка удалена из базы данных." : "Ошибка: Заметка не найдена в базе данных."); + } + } + } + +} diff --git a/NoteContent/EditorPage.xaml b/NoteContent/EditorPage.xaml new file mode 100644 index 0000000..d16f0d2 --- /dev/null +++ b/NoteContent/EditorPage.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + diff --git a/NoteContent/EditorPage.xaml.cs b/NoteContent/EditorPage.xaml.cs new file mode 100644 index 0000000..3594d95 --- /dev/null +++ b/NoteContent/EditorPage.xaml.cs @@ -0,0 +1,301 @@ +using CommunityToolkit.Maui.Views; +using NotesDataAnalyst.NoteContent; +using System.Text.RegularExpressions; +using System.Windows.Input; + +namespace NotesDataAnalyst +{ + public partial class EditorPage : ContentPage + { + //Âûçûâàåì ýìîäçè + public ICommand ShowEmojiPopupCommand { get; } + public ICommand ShowLinkInputCommand { get; } + + public ICommand InsertHeaderCommand { get; } + + public ICommand AddLinkCommand { get; } + + private int listItemCounter = 1; // Ñ÷¸ò÷èê äëÿ íóìåðàöèè â ñïèñêå + + + private readonly Note _note; + private readonly FileSaver _fileSaver; + private readonly string _filePath; + + public EditorPage(Note note = null) + { + InitializeComponent(); + + // Èíèöèàëèçàöèÿ êîìàíä + // Èíèöèàëèçàöèÿ êîìàíä + InsertHeaderCommand = new Command(InsertHeaderTag); + // InsertBulletCommand = new Command(InsertBulletPoint); + AddLinkCommand = new Command(AddLink); + // InsertNumberedListCommand = new Command(InsertNumberedListItem); + ShowLinkInputCommand = new Command(ShowLinkInputPopup); + //Èíèöèëèçèðóåì ýìîäçè + ShowEmojiPopupCommand = new Command(ShowEmojiSelectionPopup); + BindingContext = this; // Óñòàíàâëèâàåì êîíòåêñò ïðèâÿçêè íà òåêóùèé îáúåêò ñòðàíèöû + + _note = note ?? new Note(); // Åñëè note íå ïåðåäàí, ñîçäà¸ì íîâóþ çàìåòêó + _filePath = string.Empty; // Ïóñòîé ïóòü óêàçûâàåò íà ñîçäàíèå íîâîé çàìåòêè + + // Çàïîëíÿåì äàííûå, åñëè ýòî ñóùåñòâóþùàÿ çàìåòêà + if (_note != null) + { + categoryPicker.SelectedItem = _note.Category; + titleEditor.Text = _note.Title; + textEditor.Text = _note.Text; + + if (!string.IsNullOrEmpty(_note.Image)) + { + imageButton.Source = ImageSource.FromFile(_note.Image); + } + } + + // Íàñòðàèâàåì âûáîð êàòåãîðèè è óñòàíîâêó ôîêóñà + CategoryManager.LoadCategories(); + categoryPicker.ItemsSource = CategoryManager.Categories; + categoryPicker.SelectedItem = CategoryManager.ActiveCategory; + + string basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Notes"); + _fileSaver = new FileSaver(basePath); + } + + + // Ðàáîòàåì ñ ýìîäçè + private async void ShowEmojiSelectionPopup() + { + var emojiPopup = new EmojiSelectionPopup(); + + // Îáðàáîò÷èê âûáîðà ñìàéëèêà + emojiPopup.EmojiSelected += emoji => + { + InsertEmoji(emoji); + + // Èñïîëüçóåì Dispatcher äëÿ áåçîïàñíîãî âûçîâà Close + Application.Current.Dispatcher.Dispatch(() => + { + if (emojiPopup.Handler != null) // Ïðîâåðêà, ÷òî Popup åùå íå çàêðûò + { + emojiPopup.Close(); + } + }); + }; + + // Îòîáðàæàåì Popup + await Application.Current.MainPage.ShowPopupAsync(emojiPopup); + } + + + + + + + private void InsertEmoji(string emoji) + { + int cursorPosition = textEditor.CursorPosition; + textEditor.Text = textEditor.Text.Insert(cursorPosition, emoji); + textEditor.CursorPosition = cursorPosition + emoji.Length; + } + +/////////////////////// +/// + + /// + /// ////////////////////////////////////////////////////////////// + /// + /// + + // Òýãè ìàðêäàóí + private void InsertHeaderTag() + { + if (textEditor == null) + { + Console.WriteLine("textEditor íå èíèöèàëèçèðîâàí."); + return; + } + + // Ïðîâåðêà, åñëè òåêñò ïóñòîé + if (string.IsNullOrEmpty(textEditor.Text)) + { + textEditor.Text = "# "; + textEditor.CursorPosition = textEditor.Text.Length; + return; + } + + int cursorPosition = textEditor.CursorPosition; + string newText = cursorPosition == 0 || textEditor.Text[cursorPosition - 1] == '\n' ? "# " : "\n# "; + + textEditor.Text = textEditor.Text.Insert(cursorPosition, newText); + textEditor.CursorPosition = cursorPosition + newText.Length; + } + + private async void ShowLinkInputPopup() + { + var linkInputPopup = new LinkInputPopup(); + + // Ïîäïèñûâàåìñÿ íà ñîáûòèå, ÷òîáû ïîëó÷èòü ññûëêó è âñòàâèòü å¸ â òåêñò + linkInputPopup.LinkSubmitted += url => + { + InsertLinkMarkdown(url); + }; + + // Îòêðûâàåì ìîäàëüíî + await Application.Current.MainPage.Navigation.PushModalAsync(linkInputPopup); + } + + private void InsertLinkMarkdown(string url) + { + string markdownLink = $"![]({url})"; + int cursorPosition = textEditor.CursorPosition; + textEditor.Text = textEditor.Text.Insert(cursorPosition, markdownLink); + textEditor.CursorPosition = cursorPosition + markdownLink.Length; + } + + + + /* private void InsertBulletPoint() + { + if (textEditor == null) + { + Console.WriteLine("textEditor íå èíèöèàëèçèðîâàí."); + return; + } + + if (string.IsNullOrEmpty(textEditor.Text)) + { + textEditor.Text = "* "; + textEditor.CursorPosition = textEditor.Text.Length; + return; + } + + int cursorPosition = textEditor.CursorPosition; + string bulletText = "* "; + textEditor.Text = textEditor.Text.Insert(cursorPosition, bulletText); + textEditor.CursorPosition = cursorPosition + bulletText.Length; + + textEditor.Completed += (s, e) => + { + if (!HasTwoEmptyLines(cursorPosition)) + { + cursorPosition = textEditor.CursorPosition; + textEditor.Text = textEditor.Text.Insert(cursorPosition, bulletText); + textEditor.CursorPosition = cursorPosition + bulletText.Length; + } + }; + } + + + private void InsertNumberedListItem() + { + int cursorPosition = textEditor.CursorPosition; + string listItemText = $"{listItemCounter}. "; + + textEditor.Text = textEditor.Text.Insert(cursorPosition, listItemText); + textEditor.CursorPosition = cursorPosition + listItemText.Length; + + // Îáíîâëÿåì íîìåð ñïèñêà ïðè ïåðåõîäå íà íîâóþ ñòðîêó + textEditor.Completed += (s, e) => + { + if (!HasTwoEmptyLines(cursorPosition)) + { + cursorPosition = textEditor.CursorPosition; + listItemCounter++; + listItemText = $"{listItemCounter}. "; + textEditor.Text = textEditor.Text.Insert(cursorPosition, listItemText); + textEditor.CursorPosition = cursorPosition + listItemText.Length; + } + else + { + listItemCounter = 1; // Ñáðîñ ñ÷¸ò÷èêà ïðè äâóõ ïóñòûõ ñòðîêàõ + } + }; + } + + // Ïðîâåðêà íà äâå ïóñòûå ñòðîêè ïîñëå ïîçèöèè êóðñîðà + private bool HasTwoEmptyLines(int position) + { + string textAfterCursor = textEditor.Text.Substring(position); + return Regex.IsMatch(textAfterCursor, @"\n\s*\n\s*\n"); + }*/ + + //////////////////////////////////////////////////////////////////////////////////////////// + + private async void AddLink() + { + var popup = new NoteSelectionPopup(); + popup.NoteSelected += OnNoteSelectedForLink; + await this.ShowPopupAsync(popup); + + } + + private void OnNoteSelectedForLink(Note selectedNote) + { + textEditor.Text = NoteLinkManager.InsertLink(textEditor.Text, selectedNote); + } + + + private async void OnImageTapped(object sender, EventArgs e) + { + try + { + var result = await FilePicker.PickAsync(new PickOptions + { + FileTypes = FilePickerFileType.Images, + PickerTitle = "Âûáåðèòå èçîáðàæåíèå" + }); + + if (result != null) + { + _note.Image = result.FullPath; + imageButton.Source = ImageSource.FromFile(result.FullPath); + } + } + catch (Exception ex) + { + await DisplayAlert("Îøèáêà", $"Íå óäàëîñü çàãðóçèòü èçîáðàæåíèå: {ex.Message}", "OK"); + } + } + + private async void SaveButton_Clicked(object sender, EventArgs e) + { + // Åñëè ó çàìåòêè íåò Id, ïðèñâàèâàåì íîâûé Id + if (string.IsNullOrEmpty(_note.Id)) + { + _note.Id = Guid.NewGuid().ToString(); + } + + var note = new Note + { Id = _note.Id, + Category = categoryPicker.SelectedItem as string, + Title = titleEditor.Text, + Image = _note.Image, + Text = textEditor.Text, + FilePath = _filePath + }; + // Ïðîâåðêà çàãîëîâêà + if (string.IsNullOrEmpty(note.Title)) + { + // Ïîëó÷àåì ïåðâûå 4 ñëîâà èç òåêñòà + var words = note.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries); + note.Title = string.Join(" ", words.Take(4)); + } + + //Çàïèñü â ÁÄ ïîêà îòêëþ÷åíà + await Task.Run(() => App.Database.SaveNote(note)); + // Îòïðàâëÿåì ñîîáùåíèå äëÿ îáíîâëåíèÿ ñïèñêà çàìåòîê íà ãëàâíîé ñòðàíèöå + MessagingCenter.Send(this, "NoteSaved", _note); + await DisplayAlert("Success", "Note saved successfully.", "OK"); + await Navigation.PushAsync(new MainPage()); + } + + + } +} + + + + + + diff --git a/NoteContent/EmojiSelectionPopup.xaml b/NoteContent/EmojiSelectionPopup.xaml new file mode 100644 index 0000000..4aad040 --- /dev/null +++ b/NoteContent/EmojiSelectionPopup.xaml @@ -0,0 +1,46 @@ + + + + + + + + +