mirror of
https://github.com/awatertrevi/xamarin-neo4j.git
synced 2026-09-22 09:05:29 +00:00
.NET MAUI upgrade
This commit is contained in:
@@ -1,15 +1,16 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net48</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Xamarin.Neo4j\Xamarin.Neo4j.csproj" />
|
<Compile Include="..\Xamarin.Neo4j\Utilities\QueryHelper.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,31 +1,12 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.Hosting;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.iOS
|
namespace Xamarin.Neo4j.iOS
|
||||||
{
|
{
|
||||||
// The UIApplicationDelegate for the application. This class is responsible for launching the
|
|
||||||
// User Interface of the application, as well as listening (and optionally responding) to
|
|
||||||
// application events from iOS.
|
|
||||||
[Register("AppDelegate")]
|
[Register("AppDelegate")]
|
||||||
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
|
public class AppDelegate : MauiUIApplicationDelegate
|
||||||
{
|
{
|
||||||
//
|
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||||
// This method is invoked when the application has loaded and is ready to run. In this
|
|
||||||
// method you should instantiate the window, load the UI into it and then make the window
|
|
||||||
// visible.
|
|
||||||
//
|
|
||||||
// You have 17 seconds to return from this method, or iOS will terminate your application.
|
|
||||||
//
|
|
||||||
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
|
|
||||||
{
|
|
||||||
Forms.Forms.Init();
|
|
||||||
|
|
||||||
LoadApplication(new App());
|
|
||||||
|
|
||||||
return base.FinishedLaunching(app, options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
//
|
||||||
|
// QueryEditorHandler.cs
|
||||||
|
//
|
||||||
|
// Trevi Awater
|
||||||
|
// 13-01-2022
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j.iOS
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using CoreGraphics;
|
||||||
|
using Foundation;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Handlers;
|
||||||
|
using Microsoft.Maui.Platform;
|
||||||
|
using UIKit;
|
||||||
|
using Xamarin.Neo4j.Controls;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.iOS.CustomRenderers
|
||||||
|
{
|
||||||
|
public class QueryEditorHandler : EditorHandler
|
||||||
|
{
|
||||||
|
private readonly string[] _keyWords =
|
||||||
|
[
|
||||||
|
// Clauses
|
||||||
|
"CALL", "CREATE", "DELETE", "DETACH", "FOREACH", "LOAD", "MATCH", "MERGE", "OPTIONAL", "REMOVE", "RETURN", "SET", "START", "UNION", "UNWIND", "WITH",
|
||||||
|
|
||||||
|
// Subclauses
|
||||||
|
"LIMIT", "ORDER", "SKIP", "WHERE", "YIELD",
|
||||||
|
|
||||||
|
// Modifiers
|
||||||
|
"ASC", "ASCENDING", "ASSERT", "BY", "CSV", "DESC", "DESCENDING", "ON",
|
||||||
|
|
||||||
|
// Expressions
|
||||||
|
"ALL", "CASE", "COUNT", "ELSE", "END", "EXISTS", "THEN", "WHEN",
|
||||||
|
|
||||||
|
// Operators
|
||||||
|
"AND", "AS", "CONTAINS", "DISTINCT", "ENDS", "IN", "IS", "NOT", "OR", "STARTS", "XOR",
|
||||||
|
|
||||||
|
// Schema
|
||||||
|
"CONSTRAINT", "CREATE", "DROP", "EXISTS", "INDEX", "NODE", "KEY", "UNIQUE",
|
||||||
|
|
||||||
|
// Hints
|
||||||
|
"INDEX", "JOIN", "SCAN", "USING",
|
||||||
|
|
||||||
|
// Literals
|
||||||
|
"FALSE", "NULL", "TRUE"
|
||||||
|
];
|
||||||
|
|
||||||
|
protected override void ConnectHandler(MauiTextView platformView)
|
||||||
|
{
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
platformView.AutocorrectionType = UITextAutocorrectionType.No;
|
||||||
|
platformView.AutocapitalizationType = UITextAutocapitalizationType.None;
|
||||||
|
platformView.SpellCheckingType = UITextSpellCheckingType.No;
|
||||||
|
platformView.KeyboardType = UIKeyboardType.Default;
|
||||||
|
|
||||||
|
const float accessoryHeight = 58f;
|
||||||
|
const float buttonWidth = 33f;
|
||||||
|
const float executeButtonWidth = 90f;
|
||||||
|
const float padding = 8f;
|
||||||
|
const float pillHeight = 38f;
|
||||||
|
|
||||||
|
var keys = new[]
|
||||||
|
{
|
||||||
|
("(", "("), (")", ")"), ("[", "["), ("]", "]"),
|
||||||
|
(":", ":"), ("-", "-"), ("\u2192", "->"), ("\u2190", "<-")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Outer accessory — clear so system background shows through
|
||||||
|
var accessoryView = new UIView(new CGRect(0, 0, 0, accessoryHeight));
|
||||||
|
accessoryView.AutoresizingMask = UIViewAutoresizing.FlexibleWidth;
|
||||||
|
accessoryView.BackgroundColor = UIColor.Clear;
|
||||||
|
|
||||||
|
// Pill container for scroll view
|
||||||
|
var pillContainer = new UIView();
|
||||||
|
pillContainer.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||||
|
pillContainer.BackgroundColor = UIColor.SystemBackground.ColorWithAlpha(0.9f);
|
||||||
|
pillContainer.Layer.CornerRadius = pillHeight / 2f;
|
||||||
|
pillContainer.Layer.MasksToBounds = true;
|
||||||
|
|
||||||
|
var scrollView = new UIScrollView();
|
||||||
|
scrollView.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||||
|
scrollView.ShowsHorizontalScrollIndicator = false;
|
||||||
|
scrollView.ShowsVerticalScrollIndicator = false;
|
||||||
|
scrollView.Bounces = false;
|
||||||
|
scrollView.BackgroundColor = UIColor.Clear;
|
||||||
|
|
||||||
|
var xOffset = 0f;
|
||||||
|
foreach (var (label, insertion) in keys)
|
||||||
|
{
|
||||||
|
var captured = insertion;
|
||||||
|
var btn = new UIButton(UIButtonType.System);
|
||||||
|
btn.SetTitle(label, UIControlState.Normal);
|
||||||
|
btn.Frame = new CGRect(xOffset, 0, buttonWidth, pillHeight);
|
||||||
|
btn.TouchUpInside += (s, e) => platformView.InsertText(captured);
|
||||||
|
scrollView.AddSubview(btn);
|
||||||
|
xOffset += buttonWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollView.ContentSize = new CGSize(xOffset, pillHeight);
|
||||||
|
pillContainer.AddSubview(scrollView);
|
||||||
|
|
||||||
|
NSLayoutConstraint.ActivateConstraints(new[]
|
||||||
|
{
|
||||||
|
scrollView.LeadingAnchor.ConstraintEqualTo(pillContainer.LeadingAnchor),
|
||||||
|
scrollView.TrailingAnchor.ConstraintEqualTo(pillContainer.TrailingAnchor),
|
||||||
|
scrollView.TopAnchor.ConstraintEqualTo(pillContainer.TopAnchor),
|
||||||
|
scrollView.BottomAnchor.ConstraintEqualTo(pillContainer.BottomAnchor),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute button — blue pill
|
||||||
|
var executeBtn = new UIButton(UIButtonType.System);
|
||||||
|
executeBtn.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||||
|
executeBtn.SetTitle("Execute", UIControlState.Normal);
|
||||||
|
executeBtn.SetTitleColor(UIColor.White, UIControlState.Normal);
|
||||||
|
executeBtn.BackgroundColor = UIColor.SystemBlue;
|
||||||
|
executeBtn.Layer.CornerRadius = pillHeight / 2f;
|
||||||
|
executeBtn.Layer.MasksToBounds = true;
|
||||||
|
executeBtn.TouchUpInside += (s, e) =>
|
||||||
|
{
|
||||||
|
if (VirtualView is QueryEditor queryEditor)
|
||||||
|
{
|
||||||
|
queryEditor.RaiseExecuteClicked();
|
||||||
|
platformView.ResignFirstResponder();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
accessoryView.AddSubview(pillContainer);
|
||||||
|
accessoryView.AddSubview(executeBtn);
|
||||||
|
|
||||||
|
NSLayoutConstraint.ActivateConstraints(new[]
|
||||||
|
{
|
||||||
|
executeBtn.TrailingAnchor.ConstraintEqualTo(accessoryView.TrailingAnchor, -padding),
|
||||||
|
executeBtn.CenterYAnchor.ConstraintEqualTo(accessoryView.CenterYAnchor),
|
||||||
|
executeBtn.HeightAnchor.ConstraintEqualTo(pillHeight),
|
||||||
|
executeBtn.WidthAnchor.ConstraintEqualTo(executeButtonWidth),
|
||||||
|
|
||||||
|
pillContainer.LeadingAnchor.ConstraintEqualTo(accessoryView.LeadingAnchor, padding),
|
||||||
|
pillContainer.TrailingAnchor.ConstraintEqualTo(executeBtn.LeadingAnchor, -padding),
|
||||||
|
pillContainer.CenterYAnchor.ConstraintEqualTo(accessoryView.CenterYAnchor),
|
||||||
|
pillContainer.HeightAnchor.ConstraintEqualTo(pillHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
platformView.InputAccessoryView = accessoryView;
|
||||||
|
|
||||||
|
platformView.Changed += (s, e) => HighlightWords(platformView, _keyWords);
|
||||||
|
|
||||||
|
HighlightWords(platformView, _keyWords);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HighlightWords(UITextView platformView, IEnumerable<string> wordsToHighlight)
|
||||||
|
{
|
||||||
|
var text = PreprocessText(platformView.Text ?? string.Empty);
|
||||||
|
var attributedText = new NSMutableAttributedString(text);
|
||||||
|
|
||||||
|
foreach (var word in wordsToHighlight)
|
||||||
|
{
|
||||||
|
var regex = new Regex("\\b" + Regex.Escape(word) + "\\b", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
foreach (Match match in regex.Matches(text))
|
||||||
|
{
|
||||||
|
attributedText.AddAttribute(UIStringAttributeKey.ForegroundColor,
|
||||||
|
UIColor.FromRGBA(137 / 255f, 152 / 255f, 46 / 255f, 1f),
|
||||||
|
new NSRange(match.Index, match.Length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyQuoteTextColorFormatting(text, attributedText, "'(.*?)'",
|
||||||
|
UIColor.FromRGBA(174 / 255f, 139 / 255f, 45 / 255f, 1f));
|
||||||
|
|
||||||
|
ApplyQuoteTextColorFormatting(text, attributedText, "\"(.*?)\"",
|
||||||
|
UIColor.FromRGBA(174 / 255f, 139 / 255f, 45 / 255f, 1f));
|
||||||
|
|
||||||
|
var cursorPosition = platformView.SelectedRange;
|
||||||
|
platformView.AttributedText = attributedText;
|
||||||
|
platformView.SelectedRange = cursorPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyQuoteTextColorFormatting(string text, NSMutableAttributedString attributedText,
|
||||||
|
string quotePattern, UIColor color)
|
||||||
|
{
|
||||||
|
var quoteRegex = new Regex(quotePattern);
|
||||||
|
|
||||||
|
foreach (Match match in quoteRegex.Matches(text))
|
||||||
|
{
|
||||||
|
attributedText.AddAttribute(UIStringAttributeKey.ForegroundColor, color,
|
||||||
|
new NSRange(match.Index, match.Length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PreprocessText(string text)
|
||||||
|
{
|
||||||
|
text = text.Replace("\u2018", "'");
|
||||||
|
text = text.Replace("\u2019", "'");
|
||||||
|
text = text.Replace("\u201c", "\"");
|
||||||
|
text = text.Replace("\u201d", "\"");
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
//
|
|
||||||
// QueryEditorRenderer.cs
|
|
||||||
//
|
|
||||||
// Trevi Awater
|
|
||||||
// 13-01-2022
|
|
||||||
//
|
|
||||||
// © Xamarin.Neo4j.iOS
|
|
||||||
//
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using CoreGraphics;
|
|
||||||
using Foundation;
|
|
||||||
using UIKit;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Forms.Platform.iOS;
|
|
||||||
using Xamarin.Neo4j.Controls;
|
|
||||||
using Xamarin.Neo4j.iOS.CustomRenderers;
|
|
||||||
|
|
||||||
[assembly: ExportRenderer(typeof(QueryEditor), typeof(QueryEditorRenderer))]
|
|
||||||
namespace Xamarin.Neo4j.iOS.CustomRenderers
|
|
||||||
{
|
|
||||||
public class QueryEditorRenderer : EditorRenderer
|
|
||||||
{
|
|
||||||
private readonly string[] _keyWords =
|
|
||||||
{
|
|
||||||
// Clauses
|
|
||||||
"CALL", "CREATE", "DELETE", "DETACH", "FOREACH", "LOAD", "MATCH", "MERGE", "OPTIONAL", "REMOVE", "RETURN", "SET", "START", "UNION", "UNWIND", "WITH",
|
|
||||||
|
|
||||||
// Subclauses
|
|
||||||
"LIMIT", "ORDER", "SKIP", "WHERE", "YIELD",
|
|
||||||
|
|
||||||
// Modifiers
|
|
||||||
"ASC", "ASCENDING", "ASSERT", "BY", "CSV", "DESC", "DESCENDING", "ON",
|
|
||||||
|
|
||||||
// Expressions
|
|
||||||
"ALL", "CASE", "COUNT", "ELSE", "END", "EXISTS", "THEN", "WHEN",
|
|
||||||
|
|
||||||
// Operators
|
|
||||||
"AND", "AS", "CONTAINS", "DISTINCT", "ENDS", "IN", "IS", "NOT", "OR", "STARTS", "XOR",
|
|
||||||
|
|
||||||
// Schema
|
|
||||||
"CONSTRAINT", "CREATE", "DROP", "EXISTS", "INDEX", "NODE", "KEY", "UNIQUE",
|
|
||||||
|
|
||||||
// Hints
|
|
||||||
"INDEX", "JOIN", "SCAN", "USING",
|
|
||||||
|
|
||||||
// Literals
|
|
||||||
"FALSE", "NULL", "TRUE"
|
|
||||||
};
|
|
||||||
|
|
||||||
protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
|
|
||||||
{
|
|
||||||
base.OnElementChanged(e);
|
|
||||||
|
|
||||||
if (e.NewElement != null)
|
|
||||||
{
|
|
||||||
Element.TextChanged += (sender, args) =>
|
|
||||||
{
|
|
||||||
HighlightWords(_keyWords);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Control != null && Element != null)
|
|
||||||
{
|
|
||||||
Control.AutocorrectionType = UITextAutocorrectionType.No;
|
|
||||||
Control.AutocapitalizationType = UITextAutocapitalizationType.None;
|
|
||||||
Control.SpellCheckingType = UITextSpellCheckingType.No;
|
|
||||||
Control.KeyboardType = UIKeyboardType.Default;
|
|
||||||
|
|
||||||
var executeButton = new UIBarButtonItem("Execute", UIBarButtonItemStyle.Done, (sender, args) =>
|
|
||||||
{
|
|
||||||
if (Element is QueryEditor queryEditor)
|
|
||||||
{
|
|
||||||
queryEditor.RaiseExecuteClicked();
|
|
||||||
|
|
||||||
Control.ResignFirstResponder();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var keyButtons = new[]
|
|
||||||
{
|
|
||||||
new UIBarButtonItem("(", UIBarButtonItemStyle.Plain, (sender, args) => InsertText("(")),
|
|
||||||
new UIBarButtonItem(")", UIBarButtonItemStyle.Plain, (sender, args) => InsertText(")")),
|
|
||||||
new UIBarButtonItem("[", UIBarButtonItemStyle.Plain, (sender, args) => InsertText("[")),
|
|
||||||
new UIBarButtonItem("]", UIBarButtonItemStyle.Plain, (sender, args) => InsertText("]")),
|
|
||||||
new UIBarButtonItem(":", UIBarButtonItemStyle.Plain, (sender, args) => InsertText(":")),
|
|
||||||
new UIBarButtonItem("-", UIBarButtonItemStyle.Plain, (sender, args) => InsertText("-")),
|
|
||||||
new UIBarButtonItem("\u2192", UIBarButtonItemStyle.Plain, (sender, args) => InsertText("->")),
|
|
||||||
new UIBarButtonItem("\u2190", UIBarButtonItemStyle.Plain, (sender, args) => InsertText("<-")),
|
|
||||||
|
|
||||||
new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace),
|
|
||||||
|
|
||||||
executeButton
|
|
||||||
};
|
|
||||||
|
|
||||||
var toolbar = new UIToolbar(new CGRect(0.0f, 0.0f, Control.Frame.Size.Width, 44.0f));
|
|
||||||
toolbar.Items = keyButtons;
|
|
||||||
|
|
||||||
Control.InputAccessoryView = toolbar;
|
|
||||||
|
|
||||||
HighlightWords(_keyWords);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HighlightWords(IEnumerable<string> wordsToHighlight)
|
|
||||||
{
|
|
||||||
var text = PreprocessText(Control.Text);
|
|
||||||
var attributedText = new NSMutableAttributedString(text);
|
|
||||||
|
|
||||||
// Iterate through the array and apply formatting to the editor's text.
|
|
||||||
foreach (var word in wordsToHighlight)
|
|
||||||
{
|
|
||||||
var regex = new Regex("\\b" + Regex.Escape(word) + "\\b", RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
foreach (Match match in regex.Matches(text))
|
|
||||||
{
|
|
||||||
attributedText.AddAttribute(UIStringAttributeKey.ForegroundColor, Color.FromHex("#89982e").ToUIColor(),
|
|
||||||
new NSRange(match.Index, match.Length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply text color formatting for text inside single quotes.
|
|
||||||
ApplyQuoteTextColorFormatting(text, attributedText, "'(.*?)'", Color.FromHex("#ae8b2d").ToUIColor());
|
|
||||||
|
|
||||||
// Apply text color formatting for text inside double quotes.
|
|
||||||
ApplyQuoteTextColorFormatting(text, attributedText, "\"(.*?)\"", Color.FromHex("#ae8b2d").ToUIColor());
|
|
||||||
|
|
||||||
attributedText.AddAttribute(UIStringAttributeKey.Font, UIFont.FromName("Roboto Mono", (nfloat) Element.FontSize), new NSRange(0, attributedText.Length));
|
|
||||||
|
|
||||||
Control.AttributedText = attributedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InsertText(string text)
|
|
||||||
{
|
|
||||||
Control.InsertText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ApplyQuoteTextColorFormatting(string text, NSMutableAttributedString attributedText,
|
|
||||||
string quotePattern, UIColor color)
|
|
||||||
{
|
|
||||||
var quoteRegex = new Regex(quotePattern);
|
|
||||||
|
|
||||||
foreach (Match match in quoteRegex.Matches(text))
|
|
||||||
{
|
|
||||||
attributedText.AddAttribute(UIStringAttributeKey.ForegroundColor, color,
|
|
||||||
new NSRange(match.Index, match.Length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string PreprocessText(string text)
|
|
||||||
{
|
|
||||||
// Replace single quotation marks and their curly variants with: '
|
|
||||||
text = text.Replace("‘", "'");
|
|
||||||
text = text.Replace("’", "'");
|
|
||||||
|
|
||||||
// Replace double quotation marks and their curly variants with: "
|
|
||||||
text = text.Replace("“", "\"");
|
|
||||||
text = text.Replace("”", "\"");
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>5</string>
|
<string>5</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.1.0</string>
|
<string>2.0.0</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
|
|||||||
38
Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.iOS/MauiProgram.cs
Normal file
38
Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.iOS/MauiProgram.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Maui.Controls.Hosting;
|
||||||
|
using Microsoft.Maui.Hosting;
|
||||||
|
using Xamarin.Neo4j.iOS.CustomRenderers;
|
||||||
|
using Xamarin.Neo4j.iOS.Services;
|
||||||
|
using Xamarin.Neo4j.Services;
|
||||||
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.iOS
|
||||||
|
{
|
||||||
|
public static class MauiProgram
|
||||||
|
{
|
||||||
|
public static MauiApp CreateMauiApp()
|
||||||
|
{
|
||||||
|
var builder = MauiApp.CreateBuilder();
|
||||||
|
builder
|
||||||
|
.UseMauiApp<App>()
|
||||||
|
.ConfigureFonts(fonts =>
|
||||||
|
{
|
||||||
|
fonts.AddFont("iconize-fontawesome-brands.ttf", "FontAwesome5Brands-Regular");
|
||||||
|
fonts.AddFont("iconize-fontawesome-solid.ttf", "FontAwesome5Free-Solid");
|
||||||
|
fonts.AddFont("iconize-fontawesome-regular.ttf", "FontAwesome5Free-Regular");
|
||||||
|
fonts.AddFont("roboto-mono-regular.ttf", "RobotoMono");
|
||||||
|
})
|
||||||
|
.ConfigureMauiHandlers(handlers =>
|
||||||
|
{
|
||||||
|
handlers.AddHandler<Controls.QueryEditor, QueryEditorHandler>();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ITrustManagerService, TrustManagerService>();
|
||||||
|
builder.Services.AddSingleton<IVersionService, VersionService>();
|
||||||
|
builder.Services.AddSingleton<IScreenSizeService, ScreenSizeService>();
|
||||||
|
builder.Services.AddSingleton<Neo4jService>();
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,9 @@
|
|||||||
// © Xamarin.Neo4j.iOS
|
// © Xamarin.Neo4j.iOS
|
||||||
//
|
//
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Neo4j.iOS.Services;
|
using Xamarin.Neo4j.iOS.Services;
|
||||||
using Xamarin.Neo4j.Services;
|
|
||||||
using Xamarin.Neo4j.Services.Interfaces;
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
[assembly: Dependency(typeof(ScreenSizeService))]
|
[assembly: Dependency(typeof(ScreenSizeService))]
|
||||||
|
|||||||
@@ -7,10 +7,9 @@
|
|||||||
// © Xamarin.Neo4j.iOS
|
// © Xamarin.Neo4j.iOS
|
||||||
//
|
//
|
||||||
|
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Neo4j.Driver;
|
using Neo4j.Driver;
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Neo4j.iOS.Services;
|
using Xamarin.Neo4j.iOS.Services;
|
||||||
using Xamarin.Neo4j.Services;
|
|
||||||
using Xamarin.Neo4j.Services.Interfaces;
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
[assembly: Dependency(typeof(TrustManagerService))]
|
[assembly: Dependency(typeof(TrustManagerService))]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.iOS.Services;
|
using Xamarin.Neo4j.iOS.Services;
|
||||||
using Xamarin.Neo4j.Services.Interfaces;
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
|
|||||||
@@ -1,158 +1,36 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<TargetFramework>net10.0-ios</TargetFramework>
|
||||||
<Platform Condition=" '$(Platform)' == '' ">iPhoneSimulator</Platform>
|
|
||||||
<ProductVersion>8.0.30703</ProductVersion>
|
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
|
||||||
<ProjectGuid>{544E1297-4171-4B91-8AF4-4CD5CFD18A04}</ProjectGuid>
|
|
||||||
<ProjectTypeGuids>{FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<RootNamespace>Xamarin.Neo4j.iOS</RootNamespace>
|
<UseMaui>true</UseMaui>
|
||||||
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
|
|
||||||
<AssemblyName>Xamarin.Neo4j.iOS</AssemblyName>
|
<AssemblyName>Xamarin.Neo4j.iOS</AssemblyName>
|
||||||
|
<ApplicationId>nl.resoftware.pocketgraph</ApplicationId>
|
||||||
|
<ApplicationTitle>PocketGraph</ApplicationTitle>
|
||||||
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
|
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
|
||||||
|
<SupportedOSPlatformVersion>14.2</SupportedOSPlatformVersion>
|
||||||
|
<ValidateXcodeVersion>false</ValidateXcodeVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\iPhoneSimulator\Debug</OutputPath>
|
|
||||||
<DefineConstants>DEBUG</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<ConsolePause>false</ConsolePause>
|
|
||||||
<MtouchArch>x86_64</MtouchArch>
|
|
||||||
<MtouchLink>None</MtouchLink>
|
|
||||||
<MtouchDebug>true</MtouchDebug>
|
|
||||||
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
|
|
||||||
<CodesignKey>iPhone Developer</CodesignKey>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhoneSimulator' ">
|
|
||||||
<DebugType>none</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\iPhoneSimulator\Release</OutputPath>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<MtouchLink>None</MtouchLink>
|
|
||||||
<MtouchArch>x86_64</MtouchArch>
|
|
||||||
<ConsolePause>false</ConsolePause>
|
|
||||||
<CodesignKey>iPhone Developer</CodesignKey>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' ">
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\iPhone\Debug</OutputPath>
|
|
||||||
<DefineConstants>DEBUG</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<ConsolePause>false</ConsolePause>
|
|
||||||
<MtouchArch>ARM64</MtouchArch>
|
|
||||||
<CodesignKey>iPhone Developer</CodesignKey>
|
|
||||||
<MtouchDebug>true</MtouchDebug>
|
|
||||||
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
|
|
||||||
<MtouchLink>SdkOnly</MtouchLink>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|iPhone' ">
|
|
||||||
<DebugType>none</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\iPhone\Release</OutputPath>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<MtouchArch>ARM64</MtouchArch>
|
|
||||||
<ConsolePause>false</ConsolePause>
|
|
||||||
<CodesignKey>iPhone Developer</CodesignKey>
|
|
||||||
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
|
|
||||||
<MtouchLink>SdkOnly</MtouchLink>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Ad-Hoc|iPhone' ">
|
|
||||||
<DebugType>none</DebugType>
|
|
||||||
<Optimize>True</Optimize>
|
|
||||||
<OutputPath>bin\iPhone\Ad-Hoc</OutputPath>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<ConsolePause>False</ConsolePause>
|
|
||||||
<MtouchArch>ARM64</MtouchArch>
|
|
||||||
<BuildIpa>True</BuildIpa>
|
|
||||||
<CodesignProvision>Automatic:AdHoc</CodesignProvision>
|
|
||||||
<CodesignKey>iPhone Distribution</CodesignKey>
|
|
||||||
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
|
|
||||||
<MtouchLink>SdkOnly</MtouchLink>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'AppStore|iPhone' ">
|
|
||||||
<DebugType>none</DebugType>
|
|
||||||
<Optimize>True</Optimize>
|
|
||||||
<OutputPath>bin\iPhone\AppStore</OutputPath>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<ConsolePause>False</ConsolePause>
|
|
||||||
<MtouchArch>ARM64</MtouchArch>
|
|
||||||
<CodesignProvision>Automatic:AppStore</CodesignProvision>
|
|
||||||
<CodesignKey>iPhone Distribution</CodesignKey>
|
|
||||||
<CodesignEntitlements>Entitlements.plist</CodesignEntitlements>
|
|
||||||
<MtouchLink>SdkOnly</MtouchLink>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="CustomRenderers\QueryEditorRenderer.cs" />
|
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.20" />
|
||||||
<Compile Include="Main.cs" />
|
|
||||||
<Compile Include="AppDelegate.cs" />
|
|
||||||
<Compile Include="Security\NativeTrustManager.cs" />
|
|
||||||
<Compile Include="Services\ScreenSizeService.cs" />
|
|
||||||
<Compile Include="Services\TrustManagerService.cs" />
|
|
||||||
<Compile Include="Services\VersionService.cs" />
|
|
||||||
<None Include="Entitlements.plist" />
|
|
||||||
<None Include="Info.plist" />
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-20x20@1x.png" />
|
<Compile Remove="Properties\AssemblyInfo.cs" />
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-20x20@2x-1.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-20x20@2x-2.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-20x20@2x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-20x20@3x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-29x29@1x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-29x29@2x-1.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-29x29@2x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-29x29@3x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-40x40@2x-1.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-40x40@2x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-40x40@3x-1.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-40x40@3x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-60x60@3x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-76x76@1x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-76x76@2x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-83.5@2x.png" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon-marketing-1024x1024.png" />
|
|
||||||
<InterfaceDefinition Include="LaunchScreen.storyboard" />
|
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json">
|
|
||||||
<Visible>false</Visible>
|
|
||||||
</ImageAsset>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<BundleResource Include="Resources\iconize-fontawesome-brands.ttf" />
|
||||||
<Reference Include="System.Xml" />
|
<BundleResource Include="Resources\iconize-fontawesome-regular.ttf" />
|
||||||
<Reference Include="System.Core" />
|
<BundleResource Include="Resources\iconize-fontawesome-solid.ttf" />
|
||||||
<Reference Include="Xamarin.iOS" />
|
<BundleResource Include="Resources\logo.png" />
|
||||||
|
<BundleResource Include="Resources\resoftware.png" />
|
||||||
|
<BundleResource Include="Resources\roboto-mono-regular.ttf" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2196" />
|
<ProjectReference Include="..\Xamarin.Neo4j\Xamarin.Neo4j.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<BundleResource Include="Resources\iconize-fontawesome-brands.ttf" />
|
|
||||||
<BundleResource Include="Resources\iconize-fontawesome-regular.ttf" />
|
|
||||||
<BundleResource Include="Resources\iconize-fontawesome-solid.ttf" />
|
|
||||||
<BundleResource Include="Resources\logo.png" />
|
|
||||||
<BundleResource Include="Resources\resoftware.png" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<BundleResource Include="Resources\roboto-mono-regular.ttf" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Xamarin.Neo4j\Xamarin.Neo4j.csproj">
|
|
||||||
<Project>{756232d0-dbb0-4aeb-b1cf-1f5ca12cac85}</Project>
|
|
||||||
<Name>Xamarin.Neo4j</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<Application xmlns="http://xamarin.com/schemas/2014/forms"
|
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Xamarin.Neo4j.App">
|
x:Class="Xamarin.Neo4j.App">
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.Pages;
|
using Xamarin.Neo4j.Pages;
|
||||||
using Xamarin.Neo4j.Themes;
|
using Xamarin.Neo4j.Themes;
|
||||||
|
|
||||||
@@ -19,32 +19,27 @@ namespace Xamarin.Neo4j
|
|||||||
MainPage = new NavigationPage(new RootPage());
|
MainPage = new NavigationPage(new RootPage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetTheme(AppTheme theme)
|
||||||
private void SetTheme(OSAppTheme theme)
|
|
||||||
{
|
{
|
||||||
Resources = theme switch
|
Resources = theme switch
|
||||||
{
|
{
|
||||||
OSAppTheme.Dark => new DarkTheme(),
|
AppTheme.Dark => new DarkTheme(),
|
||||||
OSAppTheme.Light => new LightTheme(),
|
AppTheme.Light => new LightTheme(),
|
||||||
|
|
||||||
_ => new LightTheme()
|
_ => new LightTheme()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void OnStart()
|
protected override void OnStart()
|
||||||
{
|
{
|
||||||
// Handle when your app starts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnSleep()
|
protected override void OnSleep()
|
||||||
{
|
{
|
||||||
// Handle when your app sleeps
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnResume()
|
protected override void OnResume()
|
||||||
{
|
{
|
||||||
// Handle when your app resumes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
|
<ViewCell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||||
x:Class="Xamarin.Neo4j.Controls.ConnectionCell">
|
x:Class="Xamarin.Neo4j.Controls.ConnectionCell">
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Controls
|
namespace Xamarin.Neo4j.Controls
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Xamarin.Neo4j.Controls.LicenseCell">
|
<ViewCell xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Xamarin.Neo4j.Controls.LicenseCell">
|
||||||
<StackLayout Padding="15, 15, 15, 0">
|
<StackLayout Padding="15, 15, 15, 0">
|
||||||
<Label FontAttributes="Bold" TextColor="#4169FF" Text="{Binding Name}">
|
<Label FontAttributes="Bold" TextColor="#4169FF" Text="{Binding Name}">
|
||||||
<Label.GestureRecognizers>
|
<Label.GestureRecognizers>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Controls
|
namespace Xamarin.Neo4j.Controls
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Controls
|
namespace Xamarin.Neo4j.Controls
|
||||||
{
|
{
|
||||||
@@ -18,18 +18,6 @@ namespace Xamarin.Neo4j.Controls
|
|||||||
|
|
||||||
public event EventHandler ExecuteClicked;
|
public event EventHandler ExecuteClicked;
|
||||||
|
|
||||||
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
|
|
||||||
{
|
|
||||||
var sizeRequest = base.OnMeasure(widthConstraint, heightConstraint);
|
|
||||||
|
|
||||||
var newHeight = sizeRequest.Request.Height;
|
|
||||||
|
|
||||||
if (newHeight > MaxHeight)
|
|
||||||
newHeight = MaxHeight;
|
|
||||||
|
|
||||||
return new SizeRequest(new Size(sizeRequest.Request.Width, newHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RaiseExecuteClicked()
|
public void RaiseExecuteClicked()
|
||||||
{
|
{
|
||||||
ExecuteClicked?.Invoke(this, EventArgs.Empty);
|
ExecuteClicked?.Invoke(this, EventArgs.Empty);
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||||
xmlns:pancakeView="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView"
|
|
||||||
Margin="0, 0, 0, 25"
|
Margin="0, 0, 0, 25"
|
||||||
x:Class="Xamarin.Neo4j.Controls.QueryResultView">
|
x:Class="Xamarin.Neo4j.Controls.QueryResultView">
|
||||||
<StackLayout Orientation="Vertical" Spacing="0">
|
<StackLayout Orientation="Vertical" Spacing="0">
|
||||||
<pancakeView:PancakeView>
|
<Border Stroke="{StaticResource QueryTextBorder}" StrokeThickness="4">
|
||||||
<pancakeView:PancakeView.Border>
|
|
||||||
<pancakeView:Border Thickness="4" Color="{StaticResource QueryTextBorder}" />
|
|
||||||
</pancakeView:PancakeView.Border>
|
|
||||||
<Label Text="{Binding Query}" Padding="8" HorizontalOptions="FillAndExpand" BackgroundColor="{StaticResource Extreme}" />
|
<Label Text="{Binding Query}" Padding="8" HorizontalOptions="FillAndExpand" BackgroundColor="{StaticResource Extreme}" />
|
||||||
</pancakeView:PancakeView>
|
</Border>
|
||||||
|
|
||||||
<WebView x:Name="graphView" HorizontalOptions="FillAndExpand" IsVisible="{Binding CanDisplayGraph}" HeightRequest="200" />
|
<WebView x:Name="graphView" HorizontalOptions="FillAndExpand" IsVisible="{Binding CanDisplayGraph}" HeightRequest="200" />
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.Maui.Controls;
|
||||||
using Acr.UserDialogs;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui;
|
||||||
|
using Neo4j.Driver;
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
|
using Xamarin.Neo4j.Services;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Pages;
|
using Xamarin.Neo4j.Pages;
|
||||||
using Xamarin.Neo4j.Utilities;
|
using Xamarin.Neo4j.Utilities;
|
||||||
|
using Query = Xamarin.Neo4j.Models.Query;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Controls
|
namespace Xamarin.Neo4j.Controls
|
||||||
{
|
{
|
||||||
@@ -27,65 +30,128 @@ namespace Xamarin.Neo4j.Controls
|
|||||||
public QueryResultView()
|
public QueryResultView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
BindingContextChanged += OnBindingContextChanged;
|
||||||
|
graphView.Navigating += OnGraphViewNavigating;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPropertyChanged(string propertyName = null)
|
private async void OnGraphViewNavigating(object sender, WebNavigatingEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnPropertyChanged(propertyName);
|
Console.WriteLine($"[Graph] Inline Navigating: {e.Url}");
|
||||||
|
|
||||||
if (propertyName == nameof(BindingContext))
|
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
||||||
|
|
||||||
|
e.Cancel = true;
|
||||||
|
|
||||||
|
var connectionString = ConnectionStringManager.ActiveConnectionString;
|
||||||
|
if (connectionString == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
ParseNeovisHtml();
|
var neo4jService = IPlatformApplication.Current.Services.GetRequiredService<Neo4jService>();
|
||||||
|
|
||||||
graphView.Source = new HtmlWebViewSource()
|
var match = System.Text.RegularExpressions.Regex.Match(e.Url, @"nodeId=(\d+)");
|
||||||
{
|
if (!match.Success || !long.TryParse(match.Groups[1].Value, out var nodeId)) return;
|
||||||
Html = _neovisHtml
|
|
||||||
};
|
var result = await neo4jService.ExpandNode(nodeId, connectionString);
|
||||||
|
|
||||||
|
if (!result.Success || result.Results == null) return;
|
||||||
|
|
||||||
|
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(result.Results, connectionString.Id);
|
||||||
|
|
||||||
|
var js = $"window.addGraphData({nodesJson}, {edgesJson}, {nodeId})";
|
||||||
|
await graphView.EvaluateJavaScriptAsync(js);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Graph] Inline expand failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBindingContextChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (BindingContext == null || graphView == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Graph] OnBindingContextChanged skipped: BindingContext={BindingContext}, graphView={graphView}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[Graph] OnBindingContextChanged fired, CanDisplayGraph={QueryResult?.CanDisplayGraph}");
|
||||||
|
|
||||||
|
ParseNeovisHtmlSafe();
|
||||||
|
|
||||||
|
Console.WriteLine($"[Graph] Setting graphView.Source, html length={_neovisHtml?.Length ?? 0}");
|
||||||
|
|
||||||
|
graphView.Source = new HtmlWebViewSource { Html = _neovisHtml };
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseNeovisHtml()
|
private void ParseNeovisHtml()
|
||||||
{
|
{
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
var resourceName = "Xamarin.Neo4j.Visualization.neovis.html";
|
var resourceName = "Xamarin.Neo4j.Visualization.visgraph.html";
|
||||||
|
|
||||||
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
var available = string.Join("\n", assembly.GetManifestResourceNames());
|
||||||
|
Console.WriteLine($"[Graph] Looking for: {resourceName}");
|
||||||
|
Console.WriteLine($"[Graph] Available resources: {available.Replace("\n", ", ")}");
|
||||||
|
|
||||||
|
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
if (stream == null)
|
||||||
|
{
|
||||||
|
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||||
|
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
|
||||||
|
$"<b>Available:</b><br>{available.Replace("\n", "<br>")}</body></html>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (stream)
|
||||||
using (var reader = new StreamReader(stream))
|
using (var reader = new StreamReader(stream))
|
||||||
{
|
{
|
||||||
var (url, isEncrypted, ignoreTrust) = QueryResult.ConnectionString.ParseHost();
|
|
||||||
|
|
||||||
var result = reader.ReadToEnd();
|
var result = reader.ReadToEnd();
|
||||||
|
var connectionId = ConnectionStringManager.ActiveConnectionString?.Id ?? Guid.Empty;
|
||||||
|
|
||||||
result = result.Replace("{{host}}", url);
|
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(QueryResult.Results, connectionId);
|
||||||
result = result.Replace("{{database}}", QueryResult.ConnectionString.Database);
|
|
||||||
result = result.Replace("{{username}}", QueryResult.ConnectionString.Username);
|
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
||||||
result = result.Replace("{{password}}", QueryResult.ConnectionString.Password);
|
result = result.Replace("{{nodes}}", nodesJson);
|
||||||
result = result.Replace("{{encryption}}", isEncrypted ? "ENCRYPTION_ON" : "ENCRYPTION_OFF");
|
result = result.Replace("{{edges}}", edgesJson);
|
||||||
result = result.Replace("{{trust}}", ignoreTrust ? "TRUST_ALL_CERTIFICATES" : "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES");
|
result = result.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
|
||||||
result = result.Replace("{{query}}", QueryResult.DisplayQuery);
|
result = result.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
|
||||||
result = result.Replace("{{backgroundColor}}", App.Current.RequestedTheme == OSAppTheme.Dark ? "#292C31" : "#FFFFFF");
|
|
||||||
|
|
||||||
_neovisHtml = result;
|
_neovisHtml = result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ParseNeovisHtmlSafe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ParseNeovisHtml();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Graph] ParseNeovisHtml threw: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||||
|
$"<b>{ex.GetType().Name}</b><br>{ex.Message}<br><br>{ex.StackTrace?.Replace("\n", "<br>")}</body></html>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void OpenNeovis(object sender, EventArgs e)
|
private async void OpenNeovis(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await Application.Current.MainPage.Navigation.PushAsync(new GraphPage(_neovisHtml));
|
var neo4jService = IPlatformApplication.Current.Services.GetRequiredService<Neo4jService>();
|
||||||
|
var connectionString = ConnectionStringManager.ActiveConnectionString;
|
||||||
|
await Application.Current.MainPage.Navigation.PushAsync(new GraphPage(_neovisHtml, connectionString, neo4jService));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SaveQuery(object sender, EventArgs e)
|
private async void SaveQuery(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var queryNameResult = await UserDialogs.Instance.PromptAsync("How should the query be called?");
|
var name = await Application.Current.MainPage.DisplayPromptAsync("Save Query", "How should the query be called?");
|
||||||
|
|
||||||
if (queryNameResult.Ok)
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
var query = new Query()
|
var query = new Query()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
QueryText = QueryResult.Query,
|
QueryText = QueryResult.Query,
|
||||||
Name = queryNameResult.Value,
|
Name = name,
|
||||||
};
|
};
|
||||||
|
|
||||||
SavedQueryManager.AddSavedQuery(query);
|
SavedQueryManager.AddSavedQuery(query);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Converters
|
namespace Xamarin.Neo4j.Converters
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices.ComTypes;
|
using System.Runtime.InteropServices.ComTypes;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Maui.Storage;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xamarin.Essentials;
|
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Managers
|
namespace Xamarin.Neo4j.Managers
|
||||||
@@ -26,7 +26,7 @@ namespace Xamarin.Neo4j.Managers
|
|||||||
|
|
||||||
public static async Task<List<Neo4jConnectionString>> GetConnectionStrings()
|
public static async Task<List<Neo4jConnectionString>> GetConnectionStrings()
|
||||||
{
|
{
|
||||||
var json = await SecureStorage.GetAsync(ConnectionStringsKey);
|
var json = await SecureStorage.Default.GetAsync(ConnectionStringsKey);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(json))
|
if (string.IsNullOrEmpty(json))
|
||||||
return new List<Neo4jConnectionString>();
|
return new List<Neo4jConnectionString>();
|
||||||
@@ -47,7 +47,7 @@ namespace Xamarin.Neo4j.Managers
|
|||||||
{
|
{
|
||||||
var json = JsonConvert.SerializeObject(connectionStrings);
|
var json = JsonConvert.SerializeObject(connectionStrings);
|
||||||
|
|
||||||
await SecureStorage.SetAsync(ConnectionStringsKey, json);
|
await SecureStorage.Default.SetAsync(ConnectionStringsKey, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task DeleteConnectionString(Neo4jConnectionString neo4JConnectionString)
|
public static async Task DeleteConnectionString(Neo4jConnectionString neo4JConnectionString)
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//
|
||||||
|
// LabelColorManager.cs
|
||||||
|
//
|
||||||
|
// Trevi Awater
|
||||||
|
// 30-03-2026
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Maui.Storage;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Managers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Persists label → color mappings per connection, so node labels keep
|
||||||
|
/// consistent colors across queries (like the Neo4j Browser).
|
||||||
|
/// </summary>
|
||||||
|
public static class LabelColorManager
|
||||||
|
{
|
||||||
|
private const string PreferenceKeyPrefix = "label_colors_";
|
||||||
|
|
||||||
|
// Neo4j Browser–style palette
|
||||||
|
private static readonly string[] Palette =
|
||||||
|
{
|
||||||
|
"#4C8EDA", // blue
|
||||||
|
"#D9534F", // red
|
||||||
|
"#57A773", // green
|
||||||
|
"#F0AD4E", // amber
|
||||||
|
"#9B59B6", // purple
|
||||||
|
"#E06CA0", // pink
|
||||||
|
"#00B5AD", // teal
|
||||||
|
"#DA7B3A", // orange
|
||||||
|
"#6477B0", // slate blue
|
||||||
|
"#C0CA33", // lime
|
||||||
|
"#7E57C2", // deep purple
|
||||||
|
"#26A69A", // cyan-teal
|
||||||
|
"#EF5350", // coral
|
||||||
|
"#5C6BC0", // indigo
|
||||||
|
"#66BB6A", // light green
|
||||||
|
"#FFA726", // deep amber
|
||||||
|
"#AB47BC", // magenta
|
||||||
|
"#29B6F6", // light blue
|
||||||
|
"#EC407A", // hot pink
|
||||||
|
"#8D6E63", // brown
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Dictionary<string, string> _cache;
|
||||||
|
private static Guid? _cachedConnectionId;
|
||||||
|
|
||||||
|
public static string GetColor(Guid connectionId, string label)
|
||||||
|
{
|
||||||
|
var map = GetMap(connectionId);
|
||||||
|
|
||||||
|
if (map.TryGetValue(label, out var color))
|
||||||
|
return color;
|
||||||
|
|
||||||
|
// Assign next unused palette color
|
||||||
|
color = Palette[map.Count % Palette.Length];
|
||||||
|
map[label] = color;
|
||||||
|
SaveMap(connectionId, map);
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> GetMap(Guid connectionId)
|
||||||
|
{
|
||||||
|
if (_cachedConnectionId == connectionId && _cache != null)
|
||||||
|
return _cache;
|
||||||
|
|
||||||
|
var key = PreferenceKeyPrefix + connectionId;
|
||||||
|
var json = Preferences.Default.Get(key, (string)null);
|
||||||
|
|
||||||
|
_cache = string.IsNullOrEmpty(json)
|
||||||
|
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
: JsonConvert.DeserializeObject<Dictionary<string, string>>(json)
|
||||||
|
?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_cachedConnectionId = connectionId;
|
||||||
|
return _cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveMap(Guid connectionId, Dictionary<string, string> map)
|
||||||
|
{
|
||||||
|
var key = PreferenceKeyPrefix + connectionId;
|
||||||
|
var json = JsonConvert.SerializeObject(map);
|
||||||
|
Preferences.Default.Set(key, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,8 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.Maui.Storage;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xamarin.Essentials;
|
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Managers
|
namespace Xamarin.Neo4j.Managers
|
||||||
@@ -22,7 +21,7 @@ namespace Xamarin.Neo4j.Managers
|
|||||||
|
|
||||||
public static List<Query> GetSavedQueries()
|
public static List<Query> GetSavedQueries()
|
||||||
{
|
{
|
||||||
var json = Preferences.Get(SavedQueriesKey, null);
|
var json = Preferences.Default.Get(SavedQueriesKey, (string)null);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(json))
|
if (string.IsNullOrEmpty(json))
|
||||||
return new List<Query>();
|
return new List<Query>();
|
||||||
@@ -43,7 +42,7 @@ namespace Xamarin.Neo4j.Managers
|
|||||||
{
|
{
|
||||||
var json = JsonConvert.SerializeObject(savedQueries);
|
var json = JsonConvert.SerializeObject(savedQueries);
|
||||||
|
|
||||||
Preferences.Set(SavedQueriesKey, json);
|
Preferences.Default.Set(SavedQueriesKey, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteSavedQuery(Query query)
|
public static void DeleteSavedQuery(Query query)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Xamarin.Essentials;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Models
|
namespace Xamarin.Neo4j.Models
|
||||||
{
|
{
|
||||||
@@ -25,6 +25,6 @@ namespace Xamarin.Neo4j.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opens the repository in the browser.
|
/// Opens the repository in the browser.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ICommand OpenRepo => new Command(() => Launcher.OpenAsync(new Uri(Repo)));
|
public ICommand OpenRepo => new Command(() => Launcher.Default.OpenAsync(new Uri(Repo)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,20 @@ namespace Xamarin.Neo4j.Models
|
|||||||
if (fullHost.StartsWith("neo4j://"))
|
if (fullHost.StartsWith("neo4j://"))
|
||||||
return new Tuple<string, bool, bool>(fullHost, false, false);
|
return new Tuple<string, bool, bool>(fullHost, false, false);
|
||||||
|
|
||||||
if (fullHost.StartsWith("bolt://"))
|
|
||||||
return new Tuple<string, bool, bool>(fullHost.Replace("bolt://", "neo4j://"), false, false);
|
|
||||||
|
|
||||||
if (fullHost.StartsWith("neo4j+s://"))
|
if (fullHost.StartsWith("neo4j+s://"))
|
||||||
return new Tuple<string, bool, bool>(fullHost.Replace("neo4j+s://", "neo4j://"), true, false);
|
return new Tuple<string, bool, bool>(fullHost, true, true);
|
||||||
|
|
||||||
if (fullHost.StartsWith("neo4j+ssc://"))
|
if (fullHost.StartsWith("neo4j+ssc://"))
|
||||||
return new Tuple<string, bool, bool>(fullHost.Replace("neo4j+ssc://", "neo4j://"), true, true);
|
return new Tuple<string, bool, bool>(fullHost, true, true);
|
||||||
|
|
||||||
|
if (fullHost.StartsWith("bolt://"))
|
||||||
|
return new Tuple<string, bool, bool>(fullHost, false, false);
|
||||||
|
|
||||||
|
if (fullHost.StartsWith("bolt+s://"))
|
||||||
|
return new Tuple<string, bool, bool>(fullHost, true, true);
|
||||||
|
|
||||||
|
if (fullHost.StartsWith("bolt+ssc://"))
|
||||||
|
return new Tuple<string, bool, bool>(fullHost, true, true);
|
||||||
|
|
||||||
throw new NotSupportedException("Unknown protocol.");
|
throw new NotSupportedException("Unknown protocol.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
Title="Add Connection"
|
Title="Add Connection"
|
||||||
x:Class="Xamarin.Neo4j.Pages.AddConnectionPage">
|
x:Class="Xamarin.Neo4j.Pages.AddConnectionPage">
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||||
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
|
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
|
||||||
@@ -40,17 +40,36 @@
|
|||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<ListView ItemsSource="{Binding ConnectionStrings}" HasUnevenRows="True" SelectionMode="None" ItemTapped="SetActive">
|
<Grid>
|
||||||
<ListView.ItemTemplate>
|
<ListView ItemsSource="{Binding ConnectionStrings}" HasUnevenRows="True" SelectionMode="None" ItemTapped="SetActive" IsVisible="{Binding HasItems}">
|
||||||
<DataTemplate>
|
<ListView.ItemTemplate>
|
||||||
<controls:ConnectionCell IsActive="{Binding ., Converter={StaticResource isActiveConnection}}">
|
<DataTemplate>
|
||||||
<controls:ConnectionCell.ContextActions>
|
<controls:ConnectionCell IsActive="{Binding ., Converter={StaticResource isActiveConnection}}">
|
||||||
<MenuItem Text="Edit" Command="{Binding BindingContext.Commands[EditConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" />
|
<controls:ConnectionCell.ContextActions>
|
||||||
<MenuItem Text="Delete" Command="{Binding BindingContext.Commands[DeleteConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" IsDestructive="true" />
|
<MenuItem Text="Edit" Command="{Binding BindingContext.Commands[EditConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" />
|
||||||
</controls:ConnectionCell.ContextActions>
|
<MenuItem Text="Delete" Command="{Binding BindingContext.Commands[DeleteConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" IsDestructive="true" />
|
||||||
</controls:ConnectionCell>
|
</controls:ConnectionCell.ContextActions>
|
||||||
</DataTemplate>
|
</controls:ConnectionCell>
|
||||||
</ListView.ItemTemplate>
|
</DataTemplate>
|
||||||
</ListView>
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<StackLayout IsVisible="{Binding IsEmpty}" VerticalOptions="Center" HorizontalOptions="Center" Spacing="12">
|
||||||
|
<Label Text="{x:Static fonts:FontAwesomeSolid.Server}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="48"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
<Label Text="No connections"
|
||||||
|
FontSize="18"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
<Label Text="Tap + to add a connection"
|
||||||
|
FontSize="14"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
</StackLayout>
|
||||||
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
Title="Graph"
|
Title="Graph"
|
||||||
x:Class="Xamarin.Neo4j.Pages.GraphPage">
|
x:Class="Xamarin.Neo4j.Pages.GraphPage">
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<WebView Source="{Binding Source}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />
|
<WebView x:Name="webView" Source="{Binding Source}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -3,10 +3,16 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
|
using Xamarin.Neo4j.Services;
|
||||||
|
using Xamarin.Neo4j.Utilities;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Pages
|
namespace Xamarin.Neo4j.Pages
|
||||||
@@ -14,11 +20,58 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
public partial class GraphPage : ContentPage
|
public partial class GraphPage : ContentPage
|
||||||
{
|
{
|
||||||
public GraphPage(string html)
|
public GraphPage(string html, Neo4jConnectionString connectionString = null, Neo4jService neo4jService = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
BindingContext = new GraphViewModel(Navigation, html);
|
BindingContext = new GraphViewModel(Navigation, html);
|
||||||
|
|
||||||
|
webView.Navigating += OnWebViewNavigating;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnWebViewNavigating(object sender, WebNavigatingEventArgs e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Graph] Navigating: {e.Url}");
|
||||||
|
|
||||||
|
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
||||||
|
|
||||||
|
e.Cancel = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var connectionString = ConnectionStringManager.ActiveConnectionString;
|
||||||
|
if (connectionString == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Graph] Expand skipped: no active connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var neo4jService = IPlatformApplication.Current.Services.GetRequiredService<Neo4jService>();
|
||||||
|
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(e.Url, @"nodeId=(\d+)");
|
||||||
|
if (!match.Success || !long.TryParse(match.Groups[1].Value, out var nodeId)) return;
|
||||||
|
|
||||||
|
Console.WriteLine($"[Graph] Expanding node {nodeId}");
|
||||||
|
|
||||||
|
var result = await neo4jService.ExpandNode(nodeId, connectionString);
|
||||||
|
|
||||||
|
if (!result.Success || result.Results == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Graph] Expand query failed: {result.ErrorMessage}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(result.Results, connectionString.Id);
|
||||||
|
|
||||||
|
Console.WriteLine($"[Graph] Pushing {nodesJson.Length} chars nodes, {edgesJson.Length} chars edges");
|
||||||
|
|
||||||
|
var js = $"window.addGraphData({nodesJson}, {edgesJson}, {nodeId})";
|
||||||
|
await webView.EvaluateJavaScriptAsync(js);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Graph] Expand failed: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls"
|
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls"
|
||||||
Title="Software Licenses"
|
Title="Software Licenses"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Pages
|
namespace Xamarin.Neo4j.Pages
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||||
Title="Queries"
|
Title="Queries"
|
||||||
@@ -14,16 +14,35 @@
|
|||||||
</ContentPage.IconImageSource>
|
</ContentPage.IconImageSource>
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<ListView ItemsSource="{Binding Queries}" ItemTapped="StartSessionWithQuery">
|
<Grid>
|
||||||
<ListView.ItemTemplate>
|
<ListView ItemsSource="{Binding Queries}" ItemTapped="StartSessionWithQuery" IsVisible="{Binding HasItems}">
|
||||||
<DataTemplate>
|
<ListView.ItemTemplate>
|
||||||
<TextCell Text="{Binding Name}" Detail="{Binding QueryText}">
|
<DataTemplate>
|
||||||
<TextCell.ContextActions>
|
<TextCell Text="{Binding Name}" Detail="{Binding QueryText}">
|
||||||
<MenuItem Text="Delete" Command="{Binding BindingContext.Commands[DeleteQuery], Source={x:Reference queriesPage} }" CommandParameter="{Binding .}" IsDestructive="true" />
|
<TextCell.ContextActions>
|
||||||
</TextCell.ContextActions>
|
<MenuItem Text="Delete" Command="{Binding BindingContext.Commands[DeleteQuery], Source={x:Reference queriesPage} }" CommandParameter="{Binding .}" IsDestructive="true" />
|
||||||
</TextCell>
|
</TextCell.ContextActions>
|
||||||
</DataTemplate>
|
</TextCell>
|
||||||
</ListView.ItemTemplate>
|
</DataTemplate>
|
||||||
</ListView>
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<StackLayout IsVisible="{Binding IsEmpty}" VerticalOptions="Center" HorizontalOptions="Center" Spacing="12">
|
||||||
|
<Label Text="{x:Static fonts:FontAwesomeSolid.Code}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="48"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
<Label Text="No saved queries"
|
||||||
|
FontSize="18"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
<Label Text="Save a query from a session to see it here"
|
||||||
|
FontSize="14"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
</StackLayout>
|
||||||
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<TabbedPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:pages="clr-namespace:Xamarin.Neo4j.Pages;assembly=Xamarin.Neo4j"
|
xmlns:pages="clr-namespace:Xamarin.Neo4j.Pages;assembly=Xamarin.Neo4j"
|
||||||
x:Class="Xamarin.Neo4j.Pages.RootPage">
|
x:Class="Xamarin.Neo4j.Pages.RootPage">
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Pages
|
namespace Xamarin.Neo4j.Pages
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||||
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
|
xmlns:controls="clr-namespace:Xamarin.Neo4j.Controls;assembly=Xamarin.Neo4j"
|
||||||
xmlns:pancakeView="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView"
|
|
||||||
x:Class="Xamarin.Neo4j.Pages.SessionPage">
|
x:Class="Xamarin.Neo4j.Pages.SessionPage">
|
||||||
<NavigationPage.TitleView>
|
<NavigationPage.TitleView>
|
||||||
<StackLayout Spacing="0">
|
<StackLayout Spacing="0">
|
||||||
@@ -37,23 +36,39 @@
|
|||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<StackLayout VerticalOptions="FillAndExpand">
|
<StackLayout VerticalOptions="FillAndExpand">
|
||||||
<pancakeView:PancakeView BackgroundColor="White" CornerRadius="2" Padding="4" Margin="16">
|
<Border BackgroundColor="White" StrokeShape="RoundRectangle 2" Padding="4" Margin="16">
|
||||||
<pancakeView:PancakeView CornerRadius="2">
|
<Border Stroke="#d8e5f1" StrokeThickness="2" StrokeShape="RoundRectangle 2">
|
||||||
<pancakeView:PancakeView.Border>
|
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery" FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
|
||||||
<pancakeView:Border Thickness="2" Color="#d8e5f1" />
|
</Border>
|
||||||
</pancakeView:PancakeView.Border>
|
</Border>
|
||||||
|
|
||||||
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery" FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
|
<Grid VerticalOptions="FillAndExpand">
|
||||||
</pancakeView:PancakeView>
|
<CollectionView x:Name="resultsCollection" ItemsSource="{Binding QueryResults}" VerticalOptions="FillAndExpand">
|
||||||
</pancakeView:PancakeView>
|
<CollectionView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<controls:QueryResultView CloseRequested="CloseResultView" />
|
||||||
|
</DataTemplate>
|
||||||
|
</CollectionView.ItemTemplate>
|
||||||
|
</CollectionView>
|
||||||
|
|
||||||
<CollectionView x:Name="resultsCollection" ItemsSource="{Binding QueryResults}" VerticalOptions="FillAndExpand">
|
<StackLayout IsVisible="{Binding IsEmpty}" VerticalOptions="Center" HorizontalOptions="Center" Spacing="12" Padding="32">
|
||||||
<CollectionView.ItemTemplate>
|
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
|
||||||
<DataTemplate>
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
<controls:QueryResultView CloseRequested="CloseResultView" />
|
FontSize="48"
|
||||||
</DataTemplate>
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
</CollectionView.ItemTemplate>
|
HorizontalOptions="Center" />
|
||||||
</CollectionView>
|
<Label Text="No results yet"
|
||||||
|
FontSize="18"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
<Label Text="Write a query and tap the play button to run it"
|
||||||
|
FontSize="14"
|
||||||
|
TextColor="{StaticResource SecondaryTextColor}"
|
||||||
|
HorizontalTextAlignment="Center"
|
||||||
|
HorizontalOptions="Center" />
|
||||||
|
</StackLayout>
|
||||||
|
</Grid>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Utilities;
|
using Xamarin.Neo4j.Utilities;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
@@ -10,8 +10,6 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
public partial class SessionPage : ContentPage
|
public partial class SessionPage : ContentPage
|
||||||
{
|
{
|
||||||
private bool _canCompleteEntry = true;
|
|
||||||
|
|
||||||
private SessionViewModel ViewModel => (SessionViewModel) BindingContext;
|
private SessionViewModel ViewModel => (SessionViewModel) BindingContext;
|
||||||
|
|
||||||
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
|
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
|
||||||
@@ -20,10 +18,10 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
|
|
||||||
BindingContext = new SessionViewModel(Navigation, connectionString, initialQuery);
|
BindingContext = new SessionViewModel(Navigation, connectionString, initialQuery);
|
||||||
|
|
||||||
MessagingCenter.Subscribe<SessionViewModel>(this, "ResetScroll", (sender) =>
|
ViewModel.ScrollToTop += (_, _) =>
|
||||||
{
|
{
|
||||||
resultsCollection.ScrollTo(0, 0, ScrollToPosition.Start, true);
|
resultsCollection.ScrollTo(0, 0, ScrollToPosition.Start, true);
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FocusDatabasePicker(object sender, EventArgs e)
|
private void FocusDatabasePicker(object sender, EventArgs e)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
xmlns:fonts="clr-namespace:Xamarin.Neo4j.Fonts;assembly=Xamarin.Neo4j"
|
||||||
Title="Settings"
|
Title="Settings"
|
||||||
@@ -13,17 +13,27 @@
|
|||||||
</ContentPage.IconImageSource>
|
</ContentPage.IconImageSource>
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<StackLayout VerticalOptions="FillAndExpand">
|
<Grid RowDefinitions="*,Auto">
|
||||||
<TableView VerticalOptions="StartAndExpand" Intent="Settings" Background="Transparent">
|
<TableView Grid.Row="0" Intent="Settings" Background="Transparent" HasUnevenRows="True">
|
||||||
<TableRoot>
|
<TableRoot>
|
||||||
<TableSection>
|
<TableSection Title="Support">
|
||||||
|
<TextCell Text="Contact Support" Command="{Binding Commands[ContactSupport]}" />
|
||||||
|
<TextCell Text="Rate PocketGraph" Command="{Binding Commands[RateApp]}" />
|
||||||
|
</TableSection>
|
||||||
|
|
||||||
|
<TableSection Title="Data">
|
||||||
|
<TextCell Text="Clear Saved Connections" Command="{Binding Commands[ClearConnections]}" />
|
||||||
|
<TextCell Text="Clear Saved Queries" Command="{Binding Commands[ClearQueries]}" />
|
||||||
|
</TableSection>
|
||||||
|
|
||||||
|
<TableSection Title="About">
|
||||||
<TextCell Text="{Binding VersionLabel}" />
|
<TextCell Text="{Binding VersionLabel}" />
|
||||||
<TextCell Text="Software Licenses" Command="{Binding Commands[OpenLicensesPage]}" />
|
<TextCell Text="Software Licenses" Command="{Binding Commands[OpenLicensesPage]}" />
|
||||||
</TableSection>
|
</TableSection>
|
||||||
</TableRoot>
|
</TableRoot>
|
||||||
</TableView>
|
</TableView>
|
||||||
|
|
||||||
<StackLayout Spacing="15" Margin="25" VerticalOptions="EndAndExpand">
|
<StackLayout Grid.Row="1" Spacing="15" Padding="25">
|
||||||
<Label Text="A product of" HorizontalOptions="Center" FontAttributes="Italic" FontSize="12" TextColor="{StaticResource SecondaryTextColor}" />
|
<Label Text="A product of" HorizontalOptions="Center" FontAttributes="Italic" FontSize="12" TextColor="{StaticResource SecondaryTextColor}" />
|
||||||
<Image Source="resoftware.png" HorizontalOptions="Center" WidthRequest="100">
|
<Image Source="resoftware.png" HorizontalOptions="Center" WidthRequest="100">
|
||||||
<Image.GestureRecognizers>
|
<Image.GestureRecognizers>
|
||||||
@@ -31,6 +41,6 @@
|
|||||||
</Image.GestureRecognizers>
|
</Image.GestureRecognizers>
|
||||||
</Image>
|
</Image>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</StackLayout>
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Pages
|
namespace Xamarin.Neo4j.Pages
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:converters="clr-namespace:Xamarin.Neo4j.Converters;assembly=Xamarin.Neo4j"
|
xmlns:converters="clr-namespace:Xamarin.Neo4j.Converters;assembly=Xamarin.Neo4j"
|
||||||
Title="{Binding Title}"
|
Title="{Binding Title}"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Neo4j.Driver;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Security
|
||||||
|
{
|
||||||
|
public class InsecureTrustManager : TrustManager
|
||||||
|
{
|
||||||
|
public override bool ValidateServerCertificate(Uri uri, X509Certificate2 certificate, X509Chain chain,
|
||||||
|
SslPolicyErrors sslPolicyErrors)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,17 +10,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Devices;
|
||||||
using Neo4j.Driver;
|
using Neo4j.Driver;
|
||||||
using Neo4jClient;
|
using Neo4jClient;
|
||||||
using Neo4jClient.Cypher;
|
using Neo4jClient.Cypher;
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
|
using Xamarin.Neo4j.Security;
|
||||||
using Xamarin.Neo4j.Services;
|
using Xamarin.Neo4j.Services;
|
||||||
using Xamarin.Neo4j.Services.Interfaces;
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
using Xamarin.Neo4j.Utilities;
|
using Xamarin.Neo4j.Utilities;
|
||||||
|
|
||||||
[assembly: Dependency(typeof(Neo4jService))]
|
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Services
|
namespace Xamarin.Neo4j.Services
|
||||||
{
|
{
|
||||||
public class Neo4jService
|
public class Neo4jService
|
||||||
@@ -29,7 +31,7 @@ namespace Xamarin.Neo4j.Services
|
|||||||
|
|
||||||
public Neo4jService()
|
public Neo4jService()
|
||||||
{
|
{
|
||||||
var trustManagerService = DependencyService.Get<ITrustManagerService>();
|
var trustManagerService = IPlatformApplication.Current.Services.GetRequiredService<ITrustManagerService>();
|
||||||
|
|
||||||
_nativeTrustManager = trustManagerService.GetNativeTrustManager();
|
_nativeTrustManager = trustManagerService.GetNativeTrustManager();
|
||||||
}
|
}
|
||||||
@@ -45,15 +47,22 @@ namespace Xamarin.Neo4j.Services
|
|||||||
var driver = GraphDatabase.Driver(url,
|
var driver = GraphDatabase.Driver(url,
|
||||||
AuthTokens.Basic(connectionString.Username, connectionString.Password), (config) =>
|
AuthTokens.Basic(connectionString.Username, connectionString.Password), (config) =>
|
||||||
{
|
{
|
||||||
config.WithEncryptionLevel(isEncrypted ? EncryptionLevel.Encrypted : EncryptionLevel.None);
|
if (!ignoreTrust && DeviceInfo.Platform == DevicePlatform.iOS)
|
||||||
|
|
||||||
if (!ignoreTrust && Device.RuntimePlatform == Device.iOS)
|
|
||||||
config.WithTrustManager(_nativeTrustManager);
|
config.WithTrustManager(_nativeTrustManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
GraphClient = new BoltGraphClient(driver);
|
GraphClient = new BoltGraphClient(driver);
|
||||||
|
|
||||||
await GraphClient.ConnectAsync();
|
try
|
||||||
|
{
|
||||||
|
await GraphClient.ConnectAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ConnectAsync runs Neo4j-specific metadata queries that non-Neo4j
|
||||||
|
// servers (e.g. Memgraph) don't support. The underlying driver is
|
||||||
|
// still connected and usable for running queries.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (ServiceUnavailableException e)
|
catch (ServiceUnavailableException e)
|
||||||
@@ -81,31 +90,48 @@ namespace Xamarin.Neo4j.Services
|
|||||||
return new Tuple<bool, string>(false, e.Message);
|
return new Tuple<bool, string>(false, e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return new Tuple<bool, string>(false, $"[{e.GetType().FullName}] {e.Message}\n\n{e.InnerException?.Message}\n\n{e.StackTrace}");
|
||||||
|
}
|
||||||
|
|
||||||
return new Tuple<bool, string>(true, "Connection was successful!");
|
return new Tuple<bool, string>(true, "Connection was successful!");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Database>> LoadDatabases()
|
public async Task<List<Database>> LoadDatabases()
|
||||||
{
|
{
|
||||||
var session = GraphClient.Driver.AsyncSession(d => d.WithDatabase("system"));
|
try
|
||||||
var cursor = await session.RunAsync("SHOW DATABASES;");
|
|
||||||
|
|
||||||
var databases = new List<Database>();
|
|
||||||
|
|
||||||
while (await cursor.FetchAsync())
|
|
||||||
{
|
{
|
||||||
var name = cursor.Current.Values["name"].As<string>();
|
var session = GraphClient.Driver.AsyncSession(d => d.WithDatabase("system"));
|
||||||
var status = cursor.Current.Values["currentStatus"].As<string>();
|
var cursor = await session.RunAsync("SHOW DATABASES;");
|
||||||
var @default = cursor.Current.Values["default"].As<bool>();
|
|
||||||
|
|
||||||
databases.Add(new Database()
|
var databases = new List<Database>();
|
||||||
|
|
||||||
|
while (await cursor.FetchAsync())
|
||||||
{
|
{
|
||||||
Name = name,
|
var name = cursor.Current.Values["name"].As<string>();
|
||||||
Status = status,
|
var status = cursor.Current.Values["currentStatus"].As<string>();
|
||||||
Default = @default
|
var @default = cursor.Current.Values["default"].As<bool>();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return databases;
|
databases.Add(new Database()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Status = status,
|
||||||
|
Default = @default
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return databases;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Memgraph and some Neo4j setups don't expose a system database.
|
||||||
|
// Fall back to a single default database.
|
||||||
|
return new List<Database>
|
||||||
|
{
|
||||||
|
new Database { Name = "memgraph", Status = "online", Default = true }
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<QueryResult> ExecuteQuery(string query, Neo4jConnectionString connectionString)
|
public async Task<QueryResult> ExecuteQuery(string query, Neo4jConnectionString connectionString)
|
||||||
@@ -171,5 +197,66 @@ namespace Xamarin.Neo4j.Services
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<QueryResult> ExpandNode(long nodeId, Neo4jConnectionString connectionString)
|
||||||
|
{
|
||||||
|
var query = "MATCH (n)-[r]-(m) WHERE id(n) = $nodeId RETURN n, r, m";
|
||||||
|
|
||||||
|
var results = new Dictionary<string, List<object>>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = GraphClient.Driver.AsyncSession(d => d.WithDatabase(connectionString.Database));
|
||||||
|
var cursor = await session.RunAsync(query, new { nodeId });
|
||||||
|
|
||||||
|
while (await cursor.FetchAsync())
|
||||||
|
{
|
||||||
|
foreach (var key in cursor.Current.Keys)
|
||||||
|
if (results.ContainsKey(key) == false)
|
||||||
|
results.Add(key, new List<object>());
|
||||||
|
|
||||||
|
foreach (var record in cursor.Current.Values)
|
||||||
|
{
|
||||||
|
var value = record.Value;
|
||||||
|
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case INode _:
|
||||||
|
results[record.Key].Add(value.As<INode>());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IRelationship _:
|
||||||
|
results[record.Key].Add(value.As<IRelationship>());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
results[record.Key].Add(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QueryResult()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Success = true,
|
||||||
|
CanDisplayGraph = true,
|
||||||
|
Query = query,
|
||||||
|
ConnectionString = connectionString,
|
||||||
|
Results = results
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (ClientException e)
|
||||||
|
{
|
||||||
|
return new QueryResult()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Success = false,
|
||||||
|
Query = query,
|
||||||
|
ConnectionString = connectionString,
|
||||||
|
ErrorMessage = e.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Xamarin.Neo4j.Themes.DarkTheme">
|
x:Class="Xamarin.Neo4j.Themes.DarkTheme">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
// © Xamarin.Neo4j
|
// © Xamarin.Neo4j
|
||||||
//
|
//
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Themes
|
namespace Xamarin.Neo4j.Themes
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Xamarin.Neo4j.Themes.Fonts">
|
x:Class="Xamarin.Neo4j.Themes.Fonts">
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Themes
|
namespace Xamarin.Neo4j.Themes
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
x:Class="Xamarin.Neo4j.Themes.LightTheme">
|
x:Class="Xamarin.Neo4j.Themes.LightTheme">
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Themes
|
namespace Xamarin.Neo4j.Themes
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//
|
||||||
|
// GraphDataHelper.cs
|
||||||
|
//
|
||||||
|
// Trevi Awater
|
||||||
|
// 30-03-2026
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Neo4j.Driver;
|
||||||
|
using Xamarin.Neo4j.Managers;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Utilities
|
||||||
|
{
|
||||||
|
public static class GraphDataHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts INode and IRelationship objects from a query result dictionary and
|
||||||
|
/// builds JSON arrays suitable for the visgraph.html canvas renderer.
|
||||||
|
/// </summary>
|
||||||
|
public static (string NodesJson, string EdgesJson) BuildJson(
|
||||||
|
Dictionary<string, List<object>> results,
|
||||||
|
Guid connectionId)
|
||||||
|
{
|
||||||
|
var nodeDict = new Dictionary<long, INode>();
|
||||||
|
var relationships = new List<IRelationship>();
|
||||||
|
|
||||||
|
foreach (var values in results.Values)
|
||||||
|
{
|
||||||
|
foreach (var obj in values)
|
||||||
|
{
|
||||||
|
switch (obj)
|
||||||
|
{
|
||||||
|
case INode node:
|
||||||
|
nodeDict[node.Id] = node;
|
||||||
|
break;
|
||||||
|
case IRelationship rel:
|
||||||
|
relationships.Add(rel);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodesJson = "[" + string.Join(",", nodeDict.Values.Select(n =>
|
||||||
|
{
|
||||||
|
var label = n.Labels.FirstOrDefault() ?? "Node";
|
||||||
|
var title = string.Join(", ", n.Properties.Select(p => $"{p.Key}: {p.Value}"));
|
||||||
|
var color = LabelColorManager.GetColor(connectionId, label);
|
||||||
|
return $"{{\"id\":{n.Id},\"label\":\"{EscapeJs(label)}\",\"title\":\"{EscapeJs(title)}\",\"color\":\"{color}\"}}";
|
||||||
|
})) + "]";
|
||||||
|
|
||||||
|
var edgesJson = "[" + string.Join(",", relationships.Select(r =>
|
||||||
|
$"{{\"from\":{r.StartNodeId},\"to\":{r.EndNodeId},\"label\":\"{EscapeJs(r.Type)}\"}}")) + "]";
|
||||||
|
|
||||||
|
return (nodesJson, edgesJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string EscapeJs(string s)
|
||||||
|
{
|
||||||
|
return s?.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "") ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.RegularExpressions;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Acr.UserDialogs;
|
using Microsoft.Maui;
|
||||||
using Xamarin.Essentials;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
@@ -41,7 +40,7 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
else
|
else
|
||||||
InitializeValues(neo4JConnectionString);
|
InitializeValues(neo4JConnectionString);
|
||||||
|
|
||||||
_neo4jService = DependencyService.Resolve<Neo4jService>();
|
_neo4jService = IPlatformApplication.Current.Services.GetRequiredService<Neo4jService>();
|
||||||
|
|
||||||
Commands.Add("Test", new Command(async () =>
|
Commands.Add("Test", new Command(async () =>
|
||||||
{
|
{
|
||||||
@@ -49,7 +48,7 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
var (_, message) = await _neo4jService.EstablishConnection(connectionString);
|
var (_, message) = await _neo4jService.EstablishConnection(connectionString);
|
||||||
|
|
||||||
await UserDialogs.Instance.AlertAsync(message);
|
await Application.Current.MainPage.DisplayAlert("", message, "OK");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Commands.Add("Save", new Command(async () =>
|
Commands.Add("Save", new Command(async () =>
|
||||||
@@ -61,12 +60,12 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var namePromptResult = await UserDialogs.Instance.PromptAsync("How do you want to name this connection?", "Save Connection", "Save", "Cancel");
|
var name = await Application.Current.MainPage.DisplayPromptAsync("Save Connection", "How do you want to name this connection?", "Save", "Cancel");
|
||||||
|
|
||||||
if (!namePromptResult.Ok || string.IsNullOrWhiteSpace(namePromptResult.Value))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
connectionString.Name = namePromptResult.Value;
|
connectionString.Name = name;
|
||||||
|
|
||||||
await ConnectionStringManager.AddConnectionString(connectionString);
|
await ConnectionStringManager.AddConnectionString(connectionString);
|
||||||
}
|
}
|
||||||
@@ -84,7 +83,7 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
await Navigation.PushAsync(new SessionPage(connectionString));
|
await Navigation.PushAsync(new SessionPage(connectionString));
|
||||||
|
|
||||||
else
|
else
|
||||||
await UserDialogs.Instance.AlertAsync(message);
|
await Application.Current.MainPage.DisplayAlert("", message, "OK");
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Acr.UserDialogs;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
@@ -54,7 +54,7 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
{
|
{
|
||||||
if (ConnectionStringManager.ActiveConnectionString == null)
|
if (ConnectionStringManager.ActiveConnectionString == null)
|
||||||
{
|
{
|
||||||
await UserDialogs.Instance.AlertAsync("Please select a connection before starting a session.");
|
await Application.Current.MainPage.DisplayAlert("", "Please select a connection before starting a session.", "OK");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,6 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
{
|
{
|
||||||
ConnectionStringManager.ActiveConnectionString = ConnectionStringManager.ActiveConnectionString?.Id == connectionString.Id ? null : connectionString;
|
ConnectionStringManager.ActiveConnectionString = ConnectionStringManager.ActiveConnectionString?.Id == connectionString.Id ? null : connectionString;
|
||||||
|
|
||||||
// HACK: This causes the Converter to re-execute. This can be done much cleaner.
|
|
||||||
LoadConnectionStrings();
|
LoadConnectionStrings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +92,15 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
_connectionStrings = value;
|
_connectionStrings = value;
|
||||||
|
|
||||||
OnPropertyChanged(nameof(ConnectionStrings));
|
OnPropertyChanged(nameof(ConnectionStrings));
|
||||||
|
OnPropertyChanged(nameof(IsEmpty));
|
||||||
|
OnPropertyChanged(nameof(HasItems));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty => !(_connectionStrings?.Any() ?? false);
|
||||||
|
|
||||||
|
public bool HasItems => !IsEmpty;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ using System.ComponentModel;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using License = Xamarin.Neo4j.Models.License;
|
using License = Xamarin.Neo4j.Models.License;
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Acr.UserDialogs;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Forms;
|
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
@@ -42,7 +42,7 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
{
|
{
|
||||||
if (ConnectionStringManager.ActiveConnectionString == null)
|
if (ConnectionStringManager.ActiveConnectionString == null)
|
||||||
{
|
{
|
||||||
await UserDialogs.Instance.AlertAsync("Please select a connection before starting a session.");
|
await Application.Current.MainPage.DisplayAlert("", "Please select a connection before starting a session.", "OK");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,9 +72,15 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
_queries = value;
|
_queries = value;
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(IsEmpty));
|
||||||
|
OnPropertyChanged(nameof(HasItems));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty => !(_queries?.Any() ?? false);
|
||||||
|
|
||||||
|
public bool HasItems => !IsEmpty;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Acr.UserDialogs;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui;
|
||||||
using Xamarin.Forms.Internals;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Pages;
|
using Xamarin.Neo4j.Pages;
|
||||||
@@ -31,6 +29,8 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
{
|
{
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
public event EventHandler ScrollToTop;
|
||||||
|
|
||||||
private readonly Neo4jService _neo4jService;
|
private readonly Neo4jService _neo4jService;
|
||||||
|
|
||||||
private readonly IScreenSizeService _screenSizeService;
|
private readonly IScreenSizeService _screenSizeService;
|
||||||
@@ -49,11 +49,12 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
|
|
||||||
_neo4jService = DependencyService.Resolve<Neo4jService>();
|
_neo4jService = IPlatformApplication.Current.Services.GetRequiredService<Neo4jService>();
|
||||||
_screenSizeService = DependencyService.Resolve<IScreenSizeService>();
|
_screenSizeService = IPlatformApplication.Current.Services.GetRequiredService<IScreenSizeService>();
|
||||||
|
|
||||||
Query = initialQuery;
|
Query = initialQuery;
|
||||||
QueryResults = new ObservableCollection<QueryResult>();
|
QueryResults = new ObservableCollection<QueryResult>();
|
||||||
|
QueryResults.CollectionChanged += (_, _) => OnPropertyChanged(nameof(IsEmpty));
|
||||||
|
|
||||||
Commands.Add("ExecuteQuery", new Command(async () =>
|
Commands.Add("ExecuteQuery", new Command(async () =>
|
||||||
{
|
{
|
||||||
@@ -68,11 +69,11 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
{
|
{
|
||||||
QueryResults.Insert(0, result);
|
QueryResults.Insert(0, result);
|
||||||
|
|
||||||
MessagingCenter.Send(this, "ResetScroll");
|
ScrollToTop?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
await UserDialogs.Instance.AlertAsync(result.ErrorMessage);
|
await Application.Current.MainPage.DisplayAlert("", result.ErrorMessage, "OK");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
InitializeConnection(connectionString);
|
InitializeConnection(connectionString);
|
||||||
@@ -84,7 +85,7 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
if (!isConnected)
|
if (!isConnected)
|
||||||
{
|
{
|
||||||
await UserDialogs.Instance.AlertAsync(message);
|
await Application.Current.MainPage.DisplayAlert("", message, "OK");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,14 +96,14 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
CurrentDatabase = AvailableDatabases.SingleOrDefault(ad => ad.Name == connectionString.Database);
|
CurrentDatabase = AvailableDatabases.SingleOrDefault(ad => ad.Name == connectionString.Database);
|
||||||
|
|
||||||
if (CurrentDatabase == null)
|
if (CurrentDatabase == null)
|
||||||
CurrentDatabase = AvailableDatabases.Single(ad => ad.Default);
|
CurrentDatabase = AvailableDatabases.SingleOrDefault(ad => ad.Default) ?? AvailableDatabases.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteQueryResult(QueryResult queryResult)
|
public void DeleteQueryResult(QueryResult queryResult)
|
||||||
{
|
{
|
||||||
var index = QueryResults.IndexOf(qr => qr.Id == queryResult.Id);
|
var item = QueryResults.FirstOrDefault(qr => qr.Id == queryResult.Id);
|
||||||
|
if (item != null)
|
||||||
QueryResults.RemoveAt(index);
|
QueryResults.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotifyPropertyChangedInvocator]
|
[NotifyPropertyChangedInvocator]
|
||||||
@@ -122,6 +123,7 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
_currentDatabase = value;
|
_currentDatabase = value;
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(CanExecuteQuery));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,11 +160,14 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
_query = value;
|
_query = value;
|
||||||
|
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(CanExecuteQuery));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanExecuteQuery => !string.IsNullOrWhiteSpace(Query) && CurrentDatabase != null;
|
public bool CanExecuteQuery => !string.IsNullOrWhiteSpace(Query) && CurrentDatabase != null;
|
||||||
|
|
||||||
|
public bool IsEmpty => QueryResults?.Count == 0;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Xamarin.Essentials;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.ApplicationModel;
|
||||||
|
using Microsoft.Maui.ApplicationModel.Communication;
|
||||||
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.Maui.Devices;
|
||||||
|
using Microsoft.Maui.Storage;
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using Xamarin.Neo4j.Pages;
|
using Xamarin.Neo4j.Pages;
|
||||||
using Xamarin.Neo4j.Services.Interfaces;
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
@@ -28,9 +33,13 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
public SettingsViewModel(INavigation navigation) : base(navigation)
|
public SettingsViewModel(INavigation navigation) : base(navigation)
|
||||||
{
|
{
|
||||||
|
_versionService = IPlatformApplication.Current.Services.GetRequiredService<IVersionService>();
|
||||||
|
|
||||||
|
VersionLabel = $"Version: {_versionService.GetVersion()} (Build: {_versionService.GetBuild()})";
|
||||||
|
|
||||||
Commands.Add("OpenReSoftwareSite", new Command(async () =>
|
Commands.Add("OpenReSoftwareSite", new Command(async () =>
|
||||||
{
|
{
|
||||||
await Launcher.OpenAsync(new Uri("https://resoftware.nl/"));
|
await Launcher.Default.OpenAsync(new Uri("https://resoftware.nl/"));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Commands.Add("OpenLicensesPage", new Command(async () =>
|
Commands.Add("OpenLicensesPage", new Command(async () =>
|
||||||
@@ -38,9 +47,68 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
await Navigation.PushAsync(new LicensesPage());
|
await Navigation.PushAsync(new LicensesPage());
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_versionService = DependencyService.Get<IVersionService>();
|
Commands.Add("ContactSupport", new Command(async () =>
|
||||||
|
{
|
||||||
|
var version = _versionService.GetVersion();
|
||||||
|
var build = _versionService.GetBuild();
|
||||||
|
var deviceModel = DeviceInfo.Model;
|
||||||
|
var manufacturer = DeviceInfo.Manufacturer;
|
||||||
|
var osVersion = DeviceInfo.VersionString;
|
||||||
|
var platform = DeviceInfo.Platform.ToString();
|
||||||
|
var deviceType = DeviceInfo.DeviceType.ToString();
|
||||||
|
var appName = AppInfo.Name;
|
||||||
|
|
||||||
VersionLabel = $"Version: {_versionService.GetVersion()} (Build: {_versionService.GetBuild()})";
|
var body = $"""
|
||||||
|
|
||||||
|
|
||||||
|
--- Device Information ---
|
||||||
|
App: {appName} v{version} (Build {build})
|
||||||
|
Device: {manufacturer} {deviceModel}
|
||||||
|
Platform: {platform} {osVersion}
|
||||||
|
Device Type: {deviceType}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var message = new EmailMessage
|
||||||
|
{
|
||||||
|
To = ["support@resoftware.nl"],
|
||||||
|
Subject = $"{appName} - Support Request",
|
||||||
|
Body = body
|
||||||
|
};
|
||||||
|
|
||||||
|
await Email.Default.ComposeAsync(message);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("RateApp", new Command(async () =>
|
||||||
|
{
|
||||||
|
await Launcher.Default.OpenAsync(
|
||||||
|
new Uri("https://apps.apple.com/app/id1604368926?action=write-review"));
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("ClearConnections", new Command(async () =>
|
||||||
|
{
|
||||||
|
var clear = await Application.Current.MainPage.DisplayAlert(
|
||||||
|
"Clear Connections",
|
||||||
|
"Are you sure you want to remove all saved connections?",
|
||||||
|
"Clear", "Cancel");
|
||||||
|
|
||||||
|
if (clear)
|
||||||
|
{
|
||||||
|
SecureStorage.Default.Remove("connection_strings");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("ClearQueries", new Command(async () =>
|
||||||
|
{
|
||||||
|
var clear = await Application.Current.MainPage.DisplayAlert(
|
||||||
|
"Clear Saved Queries",
|
||||||
|
"Are you sure you want to remove all saved queries?",
|
||||||
|
"Clear", "Cancel");
|
||||||
|
|
||||||
|
if (clear)
|
||||||
|
{
|
||||||
|
Preferences.Default.Remove("saved_queries");
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotifyPropertyChangedInvocator]
|
[NotifyPropertyChangedInvocator]
|
||||||
@@ -64,6 +132,5 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Xamarin.Forms;
|
using Microsoft.Maui.Controls;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.ViewModels
|
namespace Xamarin.Neo4j.ViewModels
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en-US">
|
|
||||||
<head>
|
|
||||||
<title>Graph</title>
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
html, body {
|
|
||||||
font: 16pt arial;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
background-color: {{backgroundColor}};
|
|
||||||
}
|
|
||||||
|
|
||||||
#viz {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script src="https://cdn.neo4jlabs.com/neovis.js/v1.5.0/neovis.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body onload="draw()">
|
|
||||||
<div id="viz"></div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
let viz;
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
const config = {
|
|
||||||
container_id: "viz",
|
|
||||||
server_url: `{{host}}`,
|
|
||||||
server_user: `{{username}}`,
|
|
||||||
server_database: `{{database}}`,
|
|
||||||
server_password: `{{password}}`,
|
|
||||||
encrypted: `{{encryption}}`,
|
|
||||||
trust: `{{trust}}`,
|
|
||||||
initial_cypher: `{{query}}`
|
|
||||||
};
|
|
||||||
|
|
||||||
viz = new NeoVis.default(config);
|
|
||||||
viz.render();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<title>Graph</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
html, body { width: 100%; height: 100%; overflow: hidden; background: {{backgroundColor}}; }
|
||||||
|
canvas { display: block; position: absolute; top: 0; left: 0; }
|
||||||
|
|
||||||
|
#popup {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: rgba(20, 20, 30, 0.95);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-family: -apple-system, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-height: 40%;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
#popup .lbl {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #7EC8E3;
|
||||||
|
}
|
||||||
|
#popup .prop { color: #ccc; }
|
||||||
|
#popup .prop b { color: #fff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="c"></canvas>
|
||||||
|
<div id="popup"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var nodesData = {{nodes}};
|
||||||
|
var edgesData = {{edges}};
|
||||||
|
|
||||||
|
if (nodesData.length === 0) {
|
||||||
|
document.body.innerHTML = "<div style='padding:24px;font-family:-apple-system,sans-serif;color:#888;font-size:15px'>No graph data — query must return nodes.</div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.getElementById('c');
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
var popup = document.getElementById('popup');
|
||||||
|
var W = 0, H = 0, dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
// ---- node/edge state ----
|
||||||
|
var nodes = nodesData.map(function (d, i) {
|
||||||
|
return { id: d.id, label: d.label || String(d.id), title: d.title || '', color: d.color || '#5A99D4', x: 0, y: 0, vx: 0, vy: 0 };
|
||||||
|
});
|
||||||
|
var edges = edgesData.map(function (d) {
|
||||||
|
return { from: d.from, to: d.to, label: d.label || '' };
|
||||||
|
});
|
||||||
|
var nodeById = {};
|
||||||
|
nodes.forEach(function (n) { nodeById[n.id] = n; });
|
||||||
|
|
||||||
|
// ---- compute degree per node ----
|
||||||
|
nodes.forEach(function (n) { n.degree = 0; });
|
||||||
|
edges.forEach(function (e) {
|
||||||
|
if (nodeById[e.from]) nodeById[e.from].degree++;
|
||||||
|
if (nodeById[e.to]) nodeById[e.to].degree++;
|
||||||
|
});
|
||||||
|
var maxDegree = 1;
|
||||||
|
nodes.forEach(function (n) { if (n.degree > maxDegree) maxDegree = n.degree; });
|
||||||
|
|
||||||
|
// ---- resize ----
|
||||||
|
function resize() {
|
||||||
|
W = document.documentElement.clientWidth || window.innerWidth || document.body.clientWidth || 300;
|
||||||
|
H = document.documentElement.clientHeight || window.innerHeight || document.body.clientHeight || 400;
|
||||||
|
canvas.style.width = W + 'px';
|
||||||
|
canvas.style.height = H + 'px';
|
||||||
|
canvas.width = W * dpr;
|
||||||
|
canvas.height = H * dpr;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// place nodes in a circle around center
|
||||||
|
var N = nodes.length;
|
||||||
|
function placeInitial() {
|
||||||
|
var r = Math.min(W, H) * (0.32 + Math.min(N / 200, 0.5));
|
||||||
|
nodes.forEach(function (n, i) {
|
||||||
|
var a = (2 * Math.PI * i) / N - Math.PI / 2;
|
||||||
|
n.x = W / 2 + r * Math.cos(a);
|
||||||
|
n.y = H / 2 + r * Math.sin(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- physics (scaled by node count) ----
|
||||||
|
var large = N > 50;
|
||||||
|
var BASE_REPULSION = large ? 15000 : 5000;
|
||||||
|
var LEAF_SPRING = large ? 60 : 60;
|
||||||
|
var HUB_SPRING = large ? 300 : 200;
|
||||||
|
var SPRING_K = 0.03;
|
||||||
|
var DAMP = large ? 0.65 : 0.8;
|
||||||
|
var GRAVITY = large ? 0.003 : 0.008;
|
||||||
|
var MAX_VEL = large ? 12 : 15;
|
||||||
|
|
||||||
|
// pre-tag hubs vs leaves for fast lookup
|
||||||
|
var HUB_THRESHOLD = Math.max(3, maxDegree * 0.3);
|
||||||
|
nodes.forEach(function (n) { n.isHub = n.degree >= HUB_THRESHOLD; });
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
var i, j, a, b, dx, dy, d, f;
|
||||||
|
|
||||||
|
// repulsion — much stronger between hubs to separate clusters
|
||||||
|
var cutoff = large ? 1500 : Infinity;
|
||||||
|
for (i = 0; i < N; i++) {
|
||||||
|
for (j = i + 1; j < N; j++) {
|
||||||
|
a = nodes[i]; b = nodes[j];
|
||||||
|
dx = b.x - a.x; dy = b.y - a.y;
|
||||||
|
d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||||
|
if (d > cutoff) continue;
|
||||||
|
var rep = BASE_REPULSION;
|
||||||
|
if (a.isHub && b.isHub) rep *= 3;
|
||||||
|
else if (a.isHub || b.isHub) rep *= 1.5;
|
||||||
|
f = rep / (d * d);
|
||||||
|
var fx = f * dx / d, fy = f * dy / d;
|
||||||
|
a.vx -= fx; a.vy -= fy;
|
||||||
|
b.vx += fx; b.vy += fy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spring attraction — short for leaves, long for hub-to-hub
|
||||||
|
edges.forEach(function (e) {
|
||||||
|
a = nodeById[e.from]; b = nodeById[e.to];
|
||||||
|
if (!a || !b) return;
|
||||||
|
// leaf-to-hub: short spring (leaf stays close as petal)
|
||||||
|
// hub-to-hub: long spring (clusters separate)
|
||||||
|
var len;
|
||||||
|
if (a.isHub && b.isHub) {
|
||||||
|
len = HUB_SPRING;
|
||||||
|
} else {
|
||||||
|
var hubDeg = Math.max(a.degree, b.degree);
|
||||||
|
len = LEAF_SPRING + (HUB_SPRING - LEAF_SPRING) * 0.3 * (hubDeg / maxDegree);
|
||||||
|
}
|
||||||
|
dx = b.x - a.x; dy = b.y - a.y;
|
||||||
|
d = Math.sqrt(dx * dx + dy * dy) || 1;
|
||||||
|
f = SPRING_K * (d - len);
|
||||||
|
var fx = f * dx / d, fy = f * dy / d;
|
||||||
|
a.vx += fx; a.vy += fy;
|
||||||
|
b.vx -= fx; b.vy -= fy;
|
||||||
|
});
|
||||||
|
|
||||||
|
// gravity toward center + damping + velocity cap
|
||||||
|
nodes.forEach(function (n) {
|
||||||
|
n.vx += (W / 2 - n.x) * GRAVITY;
|
||||||
|
n.vy += (H / 2 - n.y) * GRAVITY;
|
||||||
|
n.vx *= DAMP; n.vy *= DAMP;
|
||||||
|
var v = Math.sqrt(n.vx * n.vx + n.vy * n.vy);
|
||||||
|
if (v > MAX_VEL) { n.vx *= MAX_VEL / v; n.vy *= MAX_VEL / v; }
|
||||||
|
n.x += n.vx; n.y += n.vy;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto-fit: zoom and pan so all nodes are visible with padding
|
||||||
|
function fitToView() {
|
||||||
|
if (N === 0) return;
|
||||||
|
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
|
nodes.forEach(function (n) {
|
||||||
|
if (n.x < minX) minX = n.x;
|
||||||
|
if (n.y < minY) minY = n.y;
|
||||||
|
if (n.x > maxX) maxX = n.x;
|
||||||
|
if (n.y > maxY) maxY = n.y;
|
||||||
|
});
|
||||||
|
var pad = NODE_R * 3;
|
||||||
|
var gw = (maxX - minX) + pad * 2;
|
||||||
|
var gh = (maxY - minY) + pad * 2;
|
||||||
|
if (gw < 1) gw = 1;
|
||||||
|
if (gh < 1) gh = 1;
|
||||||
|
var newZoom = Math.min(W / gw, H / gh, 2);
|
||||||
|
newZoom = Math.max(newZoom, 0.05);
|
||||||
|
var cx = (minX + maxX) / 2;
|
||||||
|
var cy = (minY + maxY) / 2;
|
||||||
|
zoom = newZoom;
|
||||||
|
panX = W / 2 - cx * zoom;
|
||||||
|
panY = H / 2 - cy * zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- camera (pan + zoom, initial zoom based on node count) ----
|
||||||
|
var initialZoom = N <= 10 ? 1 : Math.max(0.05, 1 / Math.sqrt(N / 10));
|
||||||
|
var panX = 0, panY = 0, zoom = initialZoom;
|
||||||
|
|
||||||
|
// screen coords -> world coords
|
||||||
|
function toWorld(sx, sy) {
|
||||||
|
return { x: (sx - panX) / zoom, y: (sy - panY) / zoom };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- draw ----
|
||||||
|
var NODE_R = 20;
|
||||||
|
var NODE_SEL = '#E8A838';
|
||||||
|
var EDGE_CLR = '#666';
|
||||||
|
var TEXT_CLR = '{{textColor}}';
|
||||||
|
var selected = null;
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
ctx.save();
|
||||||
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
ctx.translate(panX, panY);
|
||||||
|
ctx.scale(zoom, zoom);
|
||||||
|
|
||||||
|
// edges
|
||||||
|
edges.forEach(function (e) {
|
||||||
|
var a = nodeById[e.from], b = nodeById[e.to];
|
||||||
|
if (!a || !b) return;
|
||||||
|
var ang = Math.atan2(b.y - a.y, b.x - a.x);
|
||||||
|
var tx = b.x - NODE_R * Math.cos(ang);
|
||||||
|
var ty = b.y - NODE_R * Math.sin(ang);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(a.x, a.y);
|
||||||
|
ctx.lineTo(tx, ty);
|
||||||
|
ctx.strokeStyle = EDGE_CLR;
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// arrowhead
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tx, ty);
|
||||||
|
ctx.lineTo(tx - 9 * Math.cos(ang - 0.4), ty - 9 * Math.sin(ang - 0.4));
|
||||||
|
ctx.lineTo(tx - 9 * Math.cos(ang + 0.4), ty - 9 * Math.sin(ang + 0.4));
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = EDGE_CLR;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
if (e.label) {
|
||||||
|
ctx.fillStyle = TEXT_CLR;
|
||||||
|
ctx.font = '10px -apple-system, sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(e.label, (a.x + b.x) / 2, (a.y + b.y) / 2 - 5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// nodes
|
||||||
|
nodes.forEach(function (n) {
|
||||||
|
var isSel = n === selected;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(n.x, n.y, NODE_R, 0, 2 * Math.PI);
|
||||||
|
ctx.fillStyle = isSel ? NODE_SEL : n.color;
|
||||||
|
ctx.fill();
|
||||||
|
if (isSel) {
|
||||||
|
ctx.strokeStyle = '#fff';
|
||||||
|
ctx.lineWidth = 2.5;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lbl = n.label.length > 10 ? n.label.slice(0, 9) + '…' : n.label;
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.font = 'bold 11px -apple-system, sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(lbl, n.x, n.y);
|
||||||
|
});
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- popup ----
|
||||||
|
function showPopup(n) {
|
||||||
|
selected = n;
|
||||||
|
var html = '<div class="lbl">' + escHtml(n.label) + '</div>';
|
||||||
|
if (n.title) {
|
||||||
|
n.title.split(', ').forEach(function (pair) {
|
||||||
|
var idx = pair.indexOf(': ');
|
||||||
|
if (idx > -1) {
|
||||||
|
html += '<div class="prop"><b>' + escHtml(pair.slice(0, idx)) + ':</b> ' + escHtml(pair.slice(idx + 2)) + '</div>';
|
||||||
|
} else {
|
||||||
|
html += '<div class="prop">' + escHtml(pair) + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html += '<div class="prop">No properties</div>';
|
||||||
|
}
|
||||||
|
popup.innerHTML = html;
|
||||||
|
popup.style.display = 'block';
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePopup() {
|
||||||
|
selected = null;
|
||||||
|
popup.style.display = 'none';
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- expand / collapse ----
|
||||||
|
var lastTapTime = 0, lastTapNode = null;
|
||||||
|
var expandedBy = {}; // nodeId -> [childIds added by expanding that node]
|
||||||
|
|
||||||
|
// initial nodes are never collapsible
|
||||||
|
var initialNodeIds = {};
|
||||||
|
nodes.forEach(function (n) { initialNodeIds[n.id] = true; });
|
||||||
|
|
||||||
|
function recomputeDegrees() {
|
||||||
|
N = nodes.length;
|
||||||
|
nodes.forEach(function (n) { n.degree = 0; });
|
||||||
|
edges.forEach(function (e) {
|
||||||
|
if (nodeById[e.from]) nodeById[e.from].degree++;
|
||||||
|
if (nodeById[e.to]) nodeById[e.to].degree++;
|
||||||
|
});
|
||||||
|
maxDegree = 1;
|
||||||
|
nodes.forEach(function (n) { if (n.degree > maxDegree) maxDegree = n.degree; });
|
||||||
|
HUB_THRESHOLD = Math.max(3, maxDegree * 0.3);
|
||||||
|
nodes.forEach(function (n) { n.isHub = n.degree >= HUB_THRESHOLD; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpandCollapse(n) {
|
||||||
|
if (expandedBy[n.id]) {
|
||||||
|
collapseNode(n);
|
||||||
|
} else {
|
||||||
|
expandNode(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandNode(n) {
|
||||||
|
var f = document.createElement('iframe');
|
||||||
|
f.style.display = 'none';
|
||||||
|
f.src = 'app://expand?nodeId=' + n.id;
|
||||||
|
document.body.appendChild(f);
|
||||||
|
setTimeout(function () { document.body.removeChild(f); }, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseNode(n) {
|
||||||
|
var childIds = expandedBy[n.id];
|
||||||
|
if (!childIds) return;
|
||||||
|
|
||||||
|
// only remove children that aren't initial nodes and aren't
|
||||||
|
// also owned by another still-expanded node
|
||||||
|
var otherOwned = {};
|
||||||
|
Object.keys(expandedBy).forEach(function (ownerId) {
|
||||||
|
if (ownerId == n.id) return;
|
||||||
|
expandedBy[ownerId].forEach(function (cid) { otherOwned[cid] = true; });
|
||||||
|
});
|
||||||
|
|
||||||
|
var toRemove = {};
|
||||||
|
childIds.forEach(function (cid) {
|
||||||
|
if (!initialNodeIds[cid] && !otherOwned[cid]) toRemove[cid] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// also collapse any nodes that were expanded from nodes we're about to remove
|
||||||
|
Object.keys(toRemove).forEach(function (rid) {
|
||||||
|
if (expandedBy[rid]) {
|
||||||
|
expandedBy[rid].forEach(function (cid) {
|
||||||
|
if (!initialNodeIds[cid] && !otherOwned[cid]) toRemove[cid] = true;
|
||||||
|
});
|
||||||
|
delete expandedBy[rid];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove edges connected to removed nodes
|
||||||
|
edges = edges.filter(function (e) {
|
||||||
|
return !toRemove[e.from] && !toRemove[e.to];
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove nodes
|
||||||
|
nodes = nodes.filter(function (nd) { return !toRemove[nd.id]; });
|
||||||
|
Object.keys(toRemove).forEach(function (rid) { delete nodeById[rid]; });
|
||||||
|
|
||||||
|
delete expandedBy[n.id];
|
||||||
|
|
||||||
|
// hide popup if the selected node was removed
|
||||||
|
if (selected && toRemove[selected.id]) hidePopup();
|
||||||
|
|
||||||
|
recomputeDegrees();
|
||||||
|
resumeLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// called from C# via EvaluateJavaScriptAsync after expand query completes
|
||||||
|
window.addGraphData = function (newNodes, newEdges, originId) {
|
||||||
|
var origin = nodeById[originId];
|
||||||
|
var ox = origin ? origin.x : W / 2;
|
||||||
|
var oy = origin ? origin.y : H / 2;
|
||||||
|
var addedIds = [];
|
||||||
|
|
||||||
|
newNodes.forEach(function (d) {
|
||||||
|
if (nodeById[d.id]) {
|
||||||
|
addedIds.push(d.id); // track even existing ones as children
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var angle = Math.random() * 2 * Math.PI;
|
||||||
|
var nd = {
|
||||||
|
id: d.id, label: d.label || String(d.id), title: d.title || '',
|
||||||
|
color: d.color || '#5A99D4',
|
||||||
|
x: ox + (40 + Math.random() * 40) * Math.cos(angle),
|
||||||
|
y: oy + (40 + Math.random() * 40) * Math.sin(angle),
|
||||||
|
vx: 0, vy: 0, degree: 0
|
||||||
|
};
|
||||||
|
nodes.push(nd);
|
||||||
|
nodeById[nd.id] = nd;
|
||||||
|
addedIds.push(d.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
newEdges.forEach(function (d) {
|
||||||
|
var exists = edges.some(function (e) {
|
||||||
|
return e.from === d.from && e.to === d.to && e.label === (d.label || '');
|
||||||
|
});
|
||||||
|
if (exists) return;
|
||||||
|
if (!nodeById[d.from] || !nodeById[d.to]) return;
|
||||||
|
edges.push({ from: d.from, to: d.to, label: d.label || '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
expandedBy[originId] = addedIds;
|
||||||
|
|
||||||
|
recomputeDegrees();
|
||||||
|
resumeLoop();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- interaction handling (touch + mouse/pointer) ----
|
||||||
|
var ptrStartX, ptrStartY, ptrEndX, ptrEndY, ptrStartTime, dragging = null, panning = false, mouseDown = false;
|
||||||
|
|
||||||
|
function nodeAt(sx, sy) {
|
||||||
|
var w = toWorld(sx, sy);
|
||||||
|
for (var i = nodes.length - 1; i >= 0; i--) {
|
||||||
|
var n = nodes[i];
|
||||||
|
if (Math.hypot(n.x - w.x, n.y - w.y) <= (NODE_R + 8) / zoom) return n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function coordFromEvent(e) {
|
||||||
|
var rect = canvas.getBoundingClientRect();
|
||||||
|
var src = e.touches ? e.touches[0] : e;
|
||||||
|
return { x: src.clientX - rect.left, y: src.clientY - rect.top };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStart(e) {
|
||||||
|
if (e.touches) e.preventDefault();
|
||||||
|
var p = coordFromEvent(e);
|
||||||
|
ptrStartX = ptrEndX = p.x;
|
||||||
|
ptrStartY = ptrEndY = p.y;
|
||||||
|
ptrStartTime = Date.now();
|
||||||
|
dragging = nodeAt(p.x, p.y);
|
||||||
|
panning = !dragging;
|
||||||
|
mouseDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMove(e) {
|
||||||
|
if (e.touches) e.preventDefault();
|
||||||
|
if (!mouseDown) return;
|
||||||
|
var p = coordFromEvent(e);
|
||||||
|
var dx = p.x - ptrEndX, dy = p.y - ptrEndY;
|
||||||
|
ptrEndX = p.x; ptrEndY = p.y;
|
||||||
|
if (panning) {
|
||||||
|
panX += dx; panY += dy;
|
||||||
|
draw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!dragging) return;
|
||||||
|
var w = toWorld(p.x, p.y);
|
||||||
|
dragging.x = w.x; dragging.y = w.y;
|
||||||
|
dragging.vx = 0; dragging.vy = 0;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnd(e) {
|
||||||
|
if (e.touches) e.preventDefault();
|
||||||
|
if (!mouseDown) return;
|
||||||
|
var dt = Date.now() - ptrStartTime;
|
||||||
|
var moved = Math.hypot(ptrEndX - ptrStartX, ptrEndY - ptrStartY) > 8;
|
||||||
|
|
||||||
|
if (!moved && dt < 300) {
|
||||||
|
var hit = nodeAt(ptrStartX, ptrStartY);
|
||||||
|
var now = Date.now();
|
||||||
|
// double-tap detection — toggle expand/collapse
|
||||||
|
if (hit && hit === lastTapNode && (now - lastTapTime) < 400) {
|
||||||
|
lastTapNode = null; lastTapTime = 0;
|
||||||
|
toggleExpandCollapse(hit);
|
||||||
|
} else if (hit) {
|
||||||
|
lastTapNode = hit; lastTapTime = now;
|
||||||
|
if (hit === selected) hidePopup();
|
||||||
|
else showPopup(hit);
|
||||||
|
} else {
|
||||||
|
lastTapNode = null; lastTapTime = 0;
|
||||||
|
hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dragging = null;
|
||||||
|
panning = false;
|
||||||
|
mouseDown = false;
|
||||||
|
resumeLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// touch events
|
||||||
|
canvas.addEventListener('touchstart', handleStart, { passive: false });
|
||||||
|
canvas.addEventListener('touchmove', handleMove, { passive: false });
|
||||||
|
canvas.addEventListener('touchend', handleEnd, { passive: false });
|
||||||
|
|
||||||
|
// mouse events (for simulator / desktop WebView)
|
||||||
|
canvas.addEventListener('mousedown', handleStart);
|
||||||
|
canvas.addEventListener('mousemove', handleMove);
|
||||||
|
canvas.addEventListener('mouseup', handleEnd);
|
||||||
|
|
||||||
|
// dismiss popup when tapping/clicking it
|
||||||
|
popup.addEventListener('touchend', function (e) { e.stopPropagation(); hidePopup(); });
|
||||||
|
popup.addEventListener('click', function (e) { e.stopPropagation(); hidePopup(); });
|
||||||
|
|
||||||
|
// ---- zoom: scroll wheel ----
|
||||||
|
canvas.addEventListener('wheel', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var rect = canvas.getBoundingClientRect();
|
||||||
|
var mx = e.clientX - rect.left, my = e.clientY - rect.top;
|
||||||
|
var factor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
||||||
|
var newZoom = Math.min(Math.max(zoom * factor, 0.1), 10);
|
||||||
|
// zoom toward mouse position
|
||||||
|
panX = mx - (mx - panX) * (newZoom / zoom);
|
||||||
|
panY = my - (my - panY) * (newZoom / zoom);
|
||||||
|
zoom = newZoom;
|
||||||
|
draw();
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// ---- zoom: pinch (two-finger touch) ----
|
||||||
|
var pinchStartDist = 0, pinchStartZoom = 1;
|
||||||
|
|
||||||
|
canvas.addEventListener('touchstart', function (e) {
|
||||||
|
if (e.touches.length === 2) {
|
||||||
|
e.preventDefault();
|
||||||
|
var dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
var dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
pinchStartDist = Math.hypot(dx, dy) || 1;
|
||||||
|
pinchStartZoom = zoom;
|
||||||
|
dragging = null;
|
||||||
|
panning = false;
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
canvas.addEventListener('touchmove', function (e) {
|
||||||
|
if (e.touches.length === 2) {
|
||||||
|
e.preventDefault();
|
||||||
|
var dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
var dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
var dist = Math.hypot(dx, dy) || 1;
|
||||||
|
var rect = canvas.getBoundingClientRect();
|
||||||
|
var mx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
|
||||||
|
var my = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
|
||||||
|
var newZoom = Math.min(Math.max(pinchStartZoom * (dist / pinchStartDist), 0.1), 10);
|
||||||
|
panX = mx - (mx - panX) * (newZoom / zoom);
|
||||||
|
panY = my - (my - panY) * (newZoom / zoom);
|
||||||
|
zoom = newZoom;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// ---- animation loop ----
|
||||||
|
var ticks = 0, rafId = null;
|
||||||
|
var maxTicks = large ? 250 : 400;
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
step();
|
||||||
|
draw();
|
||||||
|
ticks++;
|
||||||
|
if (ticks < maxTicks) rafId = requestAnimationFrame(loop);
|
||||||
|
else rafId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeLoop() {
|
||||||
|
ticks = 0;
|
||||||
|
if (!rafId) rafId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- init ----
|
||||||
|
var initRetries = 0;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
resize();
|
||||||
|
// WebView may not have layout yet — retry until we get real dimensions
|
||||||
|
if ((W <= 1 || H <= 1) && initRetries < 20) {
|
||||||
|
initRetries++;
|
||||||
|
setTimeout(init, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
placeInitial();
|
||||||
|
// center the camera on the graph with the pre-computed zoom
|
||||||
|
panX = W / 2 - (W / 2) * zoom;
|
||||||
|
panY = H / 2 - (H / 2) * zoom;
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', function () {
|
||||||
|
resize();
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use rAF to ensure the browser has laid out the canvas before we read its size
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
requestAnimationFrame(init);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net10.0-ios</TargetFramework>
|
||||||
|
<UseMaui>true</UseMaui>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@@ -11,18 +13,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Acr.UserDialogs" Version="7.2.0.534" />
|
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.20" />
|
||||||
<PackageReference Include="Neo4jClient" Version="4.1.18" />
|
<PackageReference Include="Neo4jClient" Version="4.1.18" />
|
||||||
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
|
|
||||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2196" />
|
|
||||||
<PackageReference Include="Xamarin.Forms.PancakeView" Version="2.3.0.759" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Visualization\neovis.html" />
|
<None Remove="Visualization\visgraph.html" />
|
||||||
<EmbeddedResource Include="Visualization\neovis.html" />
|
<EmbeddedResource Include="Visualization\visgraph.html" />
|
||||||
<None Remove="Resources\licenses.json" />
|
<None Remove="Resources\licenses.json" />
|
||||||
<EmbeddedResource Include="Resources\licenses.json" />
|
<EmbeddedResource Include="Resources\licenses.json" />
|
||||||
<None Remove="Assets\Fonts\RobotoMono-Regular.ttf" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user