From a2c4204b1f9b8f28581a7c3666a71ca13c06c513 Mon Sep 17 00:00:00 2001 From: ARCHI86 Date: Sat, 2 Nov 2024 19:06:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=D1=82?= =?UTF-8?q?=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.xaml | 14 + App.xaml.cs | 37 ++ AppShell.xaml | 18 + AppShell.xaml.cs | 74 +++ GraphDrawable.cs | 80 ++++ GraphNode.cs | 25 + GraphPage.xaml | 18 + GraphPage.xaml.cs | 310 +++++++++++++ MauiProgram.cs | 22 + NoteContent/CategoryManager.cs | 48 ++ NoteContent/DatabaseContext.cs | 126 ++++++ NoteContent/EditorPage.xaml | 61 +++ NoteContent/EditorPage.xaml.cs | 301 +++++++++++++ NoteContent/EmojiSelectionPopup.xaml | 46 ++ NoteContent/EmojiSelectionPopup.xaml.cs | 44 ++ NoteContent/FileLoader.cs | 70 +++ NoteContent/FileSaver.cs | 83 ++++ NoteContent/LinkInputPopup.xaml | 20 + NoteContent/LinkInputPopup.xaml.cs | 33 ++ NoteContent/MainPage.xaml | 86 ++++ NoteContent/MainPage.xaml.cs | 247 ++++++++++ NoteContent/Note.cs | 65 +++ NoteContent/NoteDetailPage.xaml | 49 ++ NoteContent/NoteDetailPage.xaml.cs | 135 ++++++ NoteContent/NoteLinkManager.cs | 48 ++ NoteContent/NoteSelectionPopup.xaml | 31 ++ NoteContent/NoteSelectionPopup.xaml.cs | 59 +++ NotesDataAnalyst.csproj | 89 ++++ NotesDataAnalyst.sln | 27 ++ Platforms/Android/AndroidManifest.xml | 6 + Platforms/Android/MainActivity.cs | 11 + Platforms/Android/MainApplication.cs | 16 + Platforms/Android/Resources/values/colors.xml | 6 + Platforms/MacCatalyst/AppDelegate.cs | 10 + Platforms/MacCatalyst/Entitlements.plist | 14 + Platforms/MacCatalyst/Info.plist | 38 ++ Platforms/MacCatalyst/Program.cs | 16 + Platforms/Tizen/Main.cs | 17 + Platforms/Tizen/tizen-manifest.xml | 15 + Platforms/Windows/App.xaml | 8 + Platforms/Windows/App.xaml.cs | 25 + Platforms/Windows/Package.appxmanifest | 46 ++ Platforms/Windows/app.manifest | 15 + Platforms/iOS/AppDelegate.cs | 10 + Platforms/iOS/Info.plist | 32 ++ Platforms/iOS/Program.cs | 16 + Properties/launchSettings.json | 8 + Resources/AppIcon/appicon.svg | 4 + Resources/AppIcon/appiconfg.svg | 8 + Resources/Fonts/OpenSans-Regular.ttf | Bin 0 -> 107184 bytes Resources/Fonts/OpenSans-Semibold.ttf | Bin 0 -> 111072 bytes Resources/Images/add.png | Bin 0 -> 1151 bytes Resources/Images/adda.png | Bin 0 -> 429 bytes Resources/Images/delete.png | Bin 0 -> 451 bytes Resources/Images/dotnet_bot.png | Bin 0 -> 69811 bytes Resources/Images/pencil.png | Bin 0 -> 801 bytes Resources/Images/upload.png | Bin 0 -> 2707 bytes Resources/Images/verified.png | Bin 0 -> 2010 bytes Resources/Images/verifiednight.png | Bin 0 -> 2440 bytes Resources/Raw/AboutAssets.txt | 15 + Resources/Splash/splash.svg | 8 + Resources/Styles/Colors.xaml | 45 ++ Resources/Styles/Styles.xaml | 426 ++++++++++++++++++ 63 files changed, 3081 insertions(+) create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 AppShell.xaml create mode 100644 AppShell.xaml.cs create mode 100644 GraphDrawable.cs create mode 100644 GraphNode.cs create mode 100644 GraphPage.xaml create mode 100644 GraphPage.xaml.cs create mode 100644 MauiProgram.cs create mode 100644 NoteContent/CategoryManager.cs create mode 100644 NoteContent/DatabaseContext.cs create mode 100644 NoteContent/EditorPage.xaml create mode 100644 NoteContent/EditorPage.xaml.cs create mode 100644 NoteContent/EmojiSelectionPopup.xaml create mode 100644 NoteContent/EmojiSelectionPopup.xaml.cs create mode 100644 NoteContent/FileLoader.cs create mode 100644 NoteContent/FileSaver.cs create mode 100644 NoteContent/LinkInputPopup.xaml create mode 100644 NoteContent/LinkInputPopup.xaml.cs create mode 100644 NoteContent/MainPage.xaml create mode 100644 NoteContent/MainPage.xaml.cs create mode 100644 NoteContent/Note.cs create mode 100644 NoteContent/NoteDetailPage.xaml create mode 100644 NoteContent/NoteDetailPage.xaml.cs create mode 100644 NoteContent/NoteLinkManager.cs create mode 100644 NoteContent/NoteSelectionPopup.xaml create mode 100644 NoteContent/NoteSelectionPopup.xaml.cs create mode 100644 NotesDataAnalyst.csproj create mode 100644 NotesDataAnalyst.sln create mode 100644 Platforms/Android/AndroidManifest.xml create mode 100644 Platforms/Android/MainActivity.cs create mode 100644 Platforms/Android/MainApplication.cs create mode 100644 Platforms/Android/Resources/values/colors.xml create mode 100644 Platforms/MacCatalyst/AppDelegate.cs create mode 100644 Platforms/MacCatalyst/Entitlements.plist create mode 100644 Platforms/MacCatalyst/Info.plist create mode 100644 Platforms/MacCatalyst/Program.cs create mode 100644 Platforms/Tizen/Main.cs create mode 100644 Platforms/Tizen/tizen-manifest.xml create mode 100644 Platforms/Windows/App.xaml create mode 100644 Platforms/Windows/App.xaml.cs create mode 100644 Platforms/Windows/Package.appxmanifest create mode 100644 Platforms/Windows/app.manifest create mode 100644 Platforms/iOS/AppDelegate.cs create mode 100644 Platforms/iOS/Info.plist create mode 100644 Platforms/iOS/Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 Resources/AppIcon/appicon.svg create mode 100644 Resources/AppIcon/appiconfg.svg create mode 100644 Resources/Fonts/OpenSans-Regular.ttf create mode 100644 Resources/Fonts/OpenSans-Semibold.ttf create mode 100644 Resources/Images/add.png create mode 100644 Resources/Images/adda.png create mode 100644 Resources/Images/delete.png create mode 100644 Resources/Images/dotnet_bot.png create mode 100644 Resources/Images/pencil.png create mode 100644 Resources/Images/upload.png create mode 100644 Resources/Images/verified.png create mode 100644 Resources/Images/verifiednight.png create mode 100644 Resources/Raw/AboutAssets.txt create mode 100644 Resources/Splash/splash.svg create mode 100644 Resources/Styles/Colors.xaml create mode 100644 Resources/Styles/Styles.xaml 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 @@ + + + + + + + + +