mirror of
https://github.com/awatertrevi/xamarin-neo4j.git
synced 2026-09-22 09:05:29 +00:00
Merge remote-tracking branch 'origin/redesign-and-android'
This commit is contained in:
105
Xamarin.Neo4j/.github/workflows/android.yml
vendored
Normal file
105
Xamarin.Neo4j/.github/workflows/android.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
name: Android
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
track:
|
||||||
|
description: 'Play Store track'
|
||||||
|
required: true
|
||||||
|
default: 'internal'
|
||||||
|
type: choice
|
||||||
|
options: [internal, alpha, beta, production]
|
||||||
|
|
||||||
|
env:
|
||||||
|
ANDROID_PROJECT: Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj
|
||||||
|
OUTPUT_DIR: Xamarin.Neo4j/Xamarin.Neo4j.Android/bin/Release/net10.0-android
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build & Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup JDK 21
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: microsoft
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Install MAUI Android workload
|
||||||
|
run: dotnet workload install maui-android
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet restore ${{ env.ANDROID_PROJECT }}
|
||||||
|
|
||||||
|
# ── Version ──────────────────────────────────────────────────────────────
|
||||||
|
- name: Resolve version
|
||||||
|
id: ver
|
||||||
|
run: |
|
||||||
|
DISPLAY="1.0.$GITHUB_RUN_NUMBER"
|
||||||
|
echo "display=$DISPLAY" >> $GITHUB_OUTPUT
|
||||||
|
echo "code=$GITHUB_RUN_NUMBER" >> $GITHUB_OUTPUT
|
||||||
|
echo "tag=v$DISPLAY" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# ── Keystore ─────────────────────────────────────────────────────────────
|
||||||
|
- name: Decode keystore
|
||||||
|
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > ${{ runner.temp }}/pocketgraph.jks
|
||||||
|
|
||||||
|
# ── Build APK (GitHub Releases) ───────────────────────────────────────────
|
||||||
|
- name: Build signed APK
|
||||||
|
run: |
|
||||||
|
dotnet publish ${{ env.ANDROID_PROJECT }} \
|
||||||
|
-c Release \
|
||||||
|
-p:AndroidPackageFormat=apk \
|
||||||
|
-p:AndroidKeyStore=true \
|
||||||
|
-p:AndroidSigningKeyStore=${{ runner.temp }}/pocketgraph.jks \
|
||||||
|
-p:AndroidSigningKeyAlias=${{ secrets.KEYSTORE_ALIAS }} \
|
||||||
|
-p:AndroidSigningKeyPass=${{ secrets.KEYSTORE_KEY_PASS }} \
|
||||||
|
-p:AndroidSigningStorePass=${{ secrets.KEYSTORE_STORE_PASS }} \
|
||||||
|
-p:ApplicationVersion=${{ steps.ver.outputs.code }} \
|
||||||
|
-p:ApplicationDisplayVersion=${{ steps.ver.outputs.display }}
|
||||||
|
|
||||||
|
# ── Build AAB (Play Store) ────────────────────────────────────────────────
|
||||||
|
- name: Build signed AAB
|
||||||
|
run: |
|
||||||
|
dotnet publish ${{ env.ANDROID_PROJECT }} \
|
||||||
|
-c Release \
|
||||||
|
-p:AndroidPackageFormat=aab \
|
||||||
|
-p:AndroidKeyStore=true \
|
||||||
|
-p:AndroidSigningKeyStore=${{ runner.temp }}/pocketgraph.jks \
|
||||||
|
-p:AndroidSigningKeyAlias=${{ secrets.KEYSTORE_ALIAS }} \
|
||||||
|
-p:AndroidSigningKeyPass=${{ secrets.KEYSTORE_KEY_PASS }} \
|
||||||
|
-p:AndroidSigningStorePass=${{ secrets.KEYSTORE_STORE_PASS }} \
|
||||||
|
-p:ApplicationVersion=${{ steps.ver.outputs.code }} \
|
||||||
|
-p:ApplicationDisplayVersion=${{ steps.ver.outputs.display }}
|
||||||
|
|
||||||
|
# ── GitHub Release ────────────────────────────────────────────────────────
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.ver.outputs.tag }}
|
||||||
|
name: PocketGraph ${{ steps.ver.outputs.display }}
|
||||||
|
generate_release_notes: true
|
||||||
|
files: ${{ env.OUTPUT_DIR }}/*-Signed.apk
|
||||||
|
|
||||||
|
# ── Play Store ────────────────────────────────────────────────────────────
|
||||||
|
- name: Upload to Play Store
|
||||||
|
uses: r0adkll/upload-google-play@v1
|
||||||
|
with:
|
||||||
|
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
|
||||||
|
packageName: nl.resoftware.pocketgraph
|
||||||
|
releaseFiles: ${{ env.OUTPUT_DIR }}/*.aab
|
||||||
|
track: ${{ github.event.inputs.track || 'internal' }}
|
||||||
|
status: completed
|
||||||
|
whatsNewDirectory: distribution/whatsnew
|
||||||
5
Xamarin.Neo4j/.gitignore
vendored
Normal file
5
Xamarin.Neo4j/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
# Android signing — never commit
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
*.env
|
||||||
@@ -15,9 +15,11 @@ Global
|
|||||||
Ad-Hoc|iPhone = Ad-Hoc|iPhone
|
Ad-Hoc|iPhone = Ad-Hoc|iPhone
|
||||||
AppStore|iPhone = AppStore|iPhone
|
AppStore|iPhone = AppStore|iPhone
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|Android = Debug|Android
|
||||||
Debug|iPhone = Debug|iPhone
|
Debug|iPhone = Debug|iPhone
|
||||||
Debug|iPhoneSimulator = Debug|iPhoneSimulator
|
Debug|iPhoneSimulator = Debug|iPhoneSimulator
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|Android = Release|Android
|
||||||
Release|iPhone = Release|iPhone
|
Release|iPhone = Release|iPhone
|
||||||
Release|iPhoneSimulator = Release|iPhoneSimulator
|
Release|iPhoneSimulator = Release|iPhoneSimulator
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
@@ -47,6 +49,9 @@ Global
|
|||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
|
||||||
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Android.ActiveCfg = Debug|Any CPU
|
||||||
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Android.Build.0 = Debug|Any CPU
|
||||||
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|Android.Deploy.0 = Debug|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.Build.0 = Debug|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.Build.0 = Debug|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.Deploy.0 = Debug|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Debug|iPhone.Deploy.0 = Debug|Any CPU
|
||||||
@@ -56,6 +61,9 @@ Global
|
|||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.Build.0 = Release|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Any CPU.Deploy.0 = Release|Any CPU
|
||||||
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Android.ActiveCfg = Release|Any CPU
|
||||||
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Android.Build.0 = Release|Any CPU
|
||||||
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|Android.Deploy.0 = Release|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.ActiveCfg = Release|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.ActiveCfg = Release|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.Build.0 = Release|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.Build.0 = Release|Any CPU
|
||||||
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.Deploy.0 = Release|Any CPU
|
{24C5C58E-C59D-4621-9BF0-2E3BAC547455}.Release|iPhone.Deploy.0 = Release|Any CPU
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
//
|
||||||
|
// QueryEditorHandler.cs
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j.Android
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Graphics;
|
||||||
|
using Android.Text;
|
||||||
|
using Android.Text.Style;
|
||||||
|
using Android.Views;
|
||||||
|
using Android.Views.InputMethods;
|
||||||
|
using Android.Widget;
|
||||||
|
using Microsoft.Maui.Handlers;
|
||||||
|
using Microsoft.Maui.Platform;
|
||||||
|
using Xamarin.Neo4j.Controls;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Android.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(MauiAppCompatEditText platformView)
|
||||||
|
{
|
||||||
|
base.ConnectHandler(platformView);
|
||||||
|
|
||||||
|
// Disable autocorrect, autocapitalize, and spellcheck
|
||||||
|
platformView.InputType = InputTypes.ClassText
|
||||||
|
| InputTypes.TextFlagMultiLine
|
||||||
|
| InputTypes.TextFlagNoSuggestions;
|
||||||
|
|
||||||
|
// Keyboard toolbar with symbol keys and Execute button
|
||||||
|
var toolbar = BuildToolbar(platformView);
|
||||||
|
platformView.FocusChange += (s, e) =>
|
||||||
|
toolbar.Visibility = e.HasFocus ? ViewStates.Visible : ViewStates.Gone;
|
||||||
|
|
||||||
|
// Syntax highlighting
|
||||||
|
platformView.AddTextChangedListener(new CypherTextWatcher(platformView, _keyWords));
|
||||||
|
HighlightSyntax(platformView, _keyWords);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout BuildToolbar(MauiAppCompatEditText platformView)
|
||||||
|
{
|
||||||
|
var context = platformView.Context;
|
||||||
|
|
||||||
|
var toolbar = new LinearLayout(context)
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MatchParent,
|
||||||
|
ViewGroup.LayoutParams.WrapContent)
|
||||||
|
};
|
||||||
|
toolbar.SetBackgroundColor(Color.ParseColor("#F2F2F7"));
|
||||||
|
toolbar.SetPadding(8, 8, 8, 8);
|
||||||
|
toolbar.Visibility = ViewStates.Gone;
|
||||||
|
|
||||||
|
// Scrollable symbol key strip
|
||||||
|
var scrollView = new HorizontalScrollView(context)
|
||||||
|
{
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, 1f)
|
||||||
|
};
|
||||||
|
scrollView.HorizontalScrollBarEnabled = false;
|
||||||
|
|
||||||
|
var keyRow = new LinearLayout(context) { Orientation = Orientation.Horizontal };
|
||||||
|
foreach (var key in new[] { "(", ")", "[", "]", ":", "-", "->", "<-" })
|
||||||
|
{
|
||||||
|
var captured = key;
|
||||||
|
var btn = CreatePillButton(context, captured);
|
||||||
|
btn.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
var start = Math.Max(platformView.SelectionStart, 0);
|
||||||
|
var end = Math.Max(platformView.SelectionEnd, 0);
|
||||||
|
platformView.EditableText?.Replace(Math.Min(start, end), Math.Max(start, end), captured);
|
||||||
|
};
|
||||||
|
keyRow.AddView(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollView.AddView(keyRow);
|
||||||
|
toolbar.AddView(scrollView);
|
||||||
|
|
||||||
|
// Execute button
|
||||||
|
var executeBtn = CreatePillButton(context, "Execute");
|
||||||
|
executeBtn.SetTextColor(Color.White);
|
||||||
|
executeBtn.SetBackgroundColor(Color.ParseColor("#007AFF"));
|
||||||
|
executeBtn.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
if (VirtualView is QueryEditor queryEditor)
|
||||||
|
{
|
||||||
|
queryEditor.RaiseExecuteClicked();
|
||||||
|
var imm = (InputMethodManager)context.GetSystemService(Context.InputMethodService);
|
||||||
|
imm?.HideSoftInputFromWindow(platformView.WindowToken, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
toolbar.AddView(executeBtn);
|
||||||
|
|
||||||
|
// Attach toolbar to the parent view hierarchy
|
||||||
|
platformView.ViewTreeObserver.GlobalLayout += (s, e) =>
|
||||||
|
{
|
||||||
|
if (platformView.Parent is ViewGroup parent && toolbar.Parent == null)
|
||||||
|
parent.AddView(toolbar);
|
||||||
|
};
|
||||||
|
|
||||||
|
return toolbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button CreatePillButton(Context context, string label)
|
||||||
|
{
|
||||||
|
return new Button(context)
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
LayoutParameters = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WrapContent,
|
||||||
|
ViewGroup.LayoutParams.WrapContent)
|
||||||
|
{
|
||||||
|
MarginStart = 4,
|
||||||
|
MarginEnd = 4
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Syntax highlighting ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
internal static void HighlightSyntax(MauiAppCompatEditText platformView, IEnumerable<string> keywords)
|
||||||
|
{
|
||||||
|
var text = PreprocessText(platformView.Text ?? string.Empty);
|
||||||
|
var spannable = new SpannableStringBuilder(text);
|
||||||
|
|
||||||
|
var keywordColor = Color.ParseColor("#899832");
|
||||||
|
var literalColor = Color.ParseColor("#AE8B2D");
|
||||||
|
|
||||||
|
foreach (var word in keywords)
|
||||||
|
{
|
||||||
|
var regex = new Regex("\\b" + Regex.Escape(word) + "\\b", RegexOptions.IgnoreCase);
|
||||||
|
foreach (Match match in regex.Matches(text))
|
||||||
|
spannable.SetSpan(new ForegroundColorSpan(keywordColor),
|
||||||
|
match.Index, match.Index + match.Length,
|
||||||
|
SpanTypes.ExclusiveExclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyQuoteHighlight(text, spannable, "'(.*?)'", literalColor);
|
||||||
|
ApplyQuoteHighlight(text, spannable, "\"(.*?)\"", literalColor);
|
||||||
|
|
||||||
|
var selStart = platformView.SelectionStart;
|
||||||
|
var selEnd = platformView.SelectionEnd;
|
||||||
|
platformView.SetText(spannable, TextView.BufferType.Spannable);
|
||||||
|
if (selStart >= 0 && selEnd <= spannable.Length())
|
||||||
|
platformView.SetSelection(selStart, selEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyQuoteHighlight(string text, SpannableStringBuilder spannable,
|
||||||
|
string pattern, Color color)
|
||||||
|
{
|
||||||
|
foreach (Match match in new Regex(pattern).Matches(text))
|
||||||
|
spannable.SetSpan(new ForegroundColorSpan(color),
|
||||||
|
match.Index, match.Index + match.Length,
|
||||||
|
SpanTypes.ExclusiveExclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PreprocessText(string text) =>
|
||||||
|
text.Replace("\u2018", "'").Replace("\u2019", "'")
|
||||||
|
.Replace("\u201c", "\"").Replace("\u201d", "\"");
|
||||||
|
|
||||||
|
// ── Inner helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private sealed class CypherTextWatcher : Java.Lang.Object, ITextWatcher
|
||||||
|
{
|
||||||
|
private readonly MauiAppCompatEditText _view;
|
||||||
|
private readonly string[] _keywords;
|
||||||
|
private bool _updating;
|
||||||
|
|
||||||
|
public CypherTextWatcher(MauiAppCompatEditText view, string[] keywords)
|
||||||
|
{
|
||||||
|
_view = view;
|
||||||
|
_keywords = keywords;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeforeTextChanged(Java.Lang.ICharSequence s, int start, int count, int after) { }
|
||||||
|
public void OnTextChanged(Java.Lang.ICharSequence s, int start, int before, int count) { }
|
||||||
|
|
||||||
|
public void AfterTextChanged(IEditable s)
|
||||||
|
{
|
||||||
|
if (_updating) return;
|
||||||
|
_updating = true;
|
||||||
|
try { HighlightSyntax(_view, _keywords); }
|
||||||
|
finally { _updating = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,74 @@
|
|||||||
using System;
|
|
||||||
using Acr.UserDialogs;
|
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Runtime;
|
using Android.Content.Res;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Widget;
|
using AndroidX.AppCompat.Widget;
|
||||||
using Android.OS;
|
using AndroidX.Core.View;
|
||||||
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.ApplicationModel;
|
||||||
|
using AColor = Android.Graphics.Color;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Android
|
namespace Xamarin.Neo4j.Android
|
||||||
{
|
{
|
||||||
[Activity(Label = "Xamarin.Neo4j", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
|
[Activity(
|
||||||
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
Theme = "@style/Maui.SplashTheme",
|
||||||
|
MainLauncher = true,
|
||||||
|
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation |
|
||||||
|
ConfigChanges.UiMode | ConfigChanges.ScreenLayout |
|
||||||
|
ConfigChanges.SmallestScreenSize | ConfigChanges.Density
|
||||||
|
)]
|
||||||
|
public class MainActivity : MauiAppCompatActivity
|
||||||
{
|
{
|
||||||
protected override void OnCreate(Bundle savedInstanceState)
|
protected override void OnResume()
|
||||||
{
|
{
|
||||||
TabLayoutResource = Resource.Layout.Tabbar;
|
base.OnResume();
|
||||||
ToolbarResource = Resource.Layout.Toolbar;
|
|
||||||
|
|
||||||
UserDialogs.Init(this);
|
// Re-apply theme and nav bar colours when returning to the foreground.
|
||||||
|
// ConfigChanges.UiMode prevents Activity recreation on system theme change,
|
||||||
|
// so RequestedThemeChanged may not fire reliably — we re-derive the theme
|
||||||
|
// from the current configuration instead.
|
||||||
|
if (IPlatformApplication.Current?.Application is App app)
|
||||||
|
{
|
||||||
|
var nightMode = Resources.Configuration.UiMode & UiMode.NightMask;
|
||||||
|
var isDark = nightMode == UiMode.NightYes;
|
||||||
|
|
||||||
base.OnCreate(savedInstanceState);
|
// Status bar: match page background, icons contrast with it
|
||||||
|
if (Window != null)
|
||||||
|
{
|
||||||
|
var statusBarColor = isDark
|
||||||
|
? AColor.ParseColor("#0c0c0c")
|
||||||
|
: AColor.ParseColor("#f5f5f5");
|
||||||
|
Window.SetStatusBarColor(statusBarColor);
|
||||||
|
|
||||||
Essentials.Platform.Init(this, savedInstanceState);
|
var controller = new WindowInsetsControllerCompat(Window, Window.DecorView);
|
||||||
|
controller.AppearanceLightStatusBars = !isDark;
|
||||||
Forms.Forms.Init(this, savedInstanceState);
|
|
||||||
|
|
||||||
LoadApplication(new App());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
{
|
app.UpdateTheme(isDark ? AppTheme.Dark : AppTheme.Light));
|
||||||
Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
|
public override void OnWindowFocusChanged(bool hasFocus)
|
||||||
|
{
|
||||||
|
base.OnWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus)
|
||||||
|
TintToolbarOverflow(Window?.DecorView as ViewGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAUI creates its toolbar programmatically so theme-based tinting doesn't reach
|
||||||
|
// the overflow icon. Walk the view tree and apply the tint directly.
|
||||||
|
private static void TintToolbarOverflow(ViewGroup? parent)
|
||||||
|
{
|
||||||
|
if (parent == null) return;
|
||||||
|
for (var i = 0; i < parent.ChildCount; i++)
|
||||||
|
{
|
||||||
|
var child = parent.GetChildAt(i);
|
||||||
|
if (child is Toolbar toolbar)
|
||||||
|
toolbar.OverflowIcon?.SetTint(AColor.White);
|
||||||
|
else if (child is ViewGroup vg)
|
||||||
|
TintToolbarOverflow(vg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Runtime;
|
||||||
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.Hosting;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Android
|
||||||
|
{
|
||||||
|
[Application]
|
||||||
|
public class MainApplication : MauiApplication
|
||||||
|
{
|
||||||
|
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
|
||||||
|
: base(handle, ownership) { }
|
||||||
|
|
||||||
|
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Maui.Controls.Hosting;
|
||||||
|
using Microsoft.Maui.Hosting;
|
||||||
|
using Xamarin.Neo4j.Android.CustomRenderers;
|
||||||
|
using Xamarin.Neo4j.Android.Services;
|
||||||
|
using Xamarin.Neo4j.Services;
|
||||||
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Android
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,26 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.Xamarin.Neo4j">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
|
android:versionCode="1"
|
||||||
<application android:label="Xamarin.Neo4j.Android"></application>
|
android:versionName="1.0"
|
||||||
|
package="nl.resoftware.pocketgraph">
|
||||||
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="36" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<!-- Required on Android 11+ to query for installed email apps -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.SENDTO" />
|
||||||
|
<data android:scheme="mailto" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="market" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<application android:label="PocketGraph" android:icon="@mipmap/appicon" android:allowBackup="true"></application>
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="windowBackground">#0c0c0c</color>
|
||||||
|
<color name="statusBarColor">#0c0c0c</color>
|
||||||
|
</resources>
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="launcher_background">#FFFFFF</color>
|
<color name="launcher_background">#0c0c0c</color>
|
||||||
<color name="colorPrimary">#3F51B5</color>
|
<color name="colorPrimary">#141414</color>
|
||||||
<color name="colorPrimaryDark">#303F9F</color>
|
<color name="colorPrimaryDark">#0c0c0c</color>
|
||||||
<color name="colorAccent">#FF4081</color>
|
<color name="colorAccent">#e2e2e2</color>
|
||||||
|
<color name="navBar">#141414</color>
|
||||||
|
<color name="windowBackground">#f5f5f5</color>
|
||||||
|
<color name="statusBarColor">#f5f5f5</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,30 +1,43 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="MainTheme" parent="MainTheme.Base">
|
<!-- Splash theme shown while MAUI initialises — must have no action bar -->
|
||||||
</style>
|
<style name="Maui.SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||||
<!-- Base theme applied no matter what API -->
|
<item name="android:windowBackground">@color/colorPrimary</item>
|
||||||
<style name="MainTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
|
<item name="android:windowNoTitle">true</item>
|
||||||
<!--If you are using revision 22.1 please use just windowNoTitle. Without android:-->
|
<item name="android:windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="android:windowFullscreen">false</item>
|
||||||
<!--We will be using the toolbar so no need to show ActionBar-->
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
<item name="windowActionBar">false</item>
|
<!-- Ensure status bar icons are visible on the splash -->
|
||||||
<!-- Set theme colors from http://www.google.com/design/spec/style/color.html#color-color-palette -->
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
<!-- colorPrimary is used for the default action bar background -->
|
|
||||||
<item name="colorPrimary">#2196F3</item>
|
|
||||||
<!-- colorPrimaryDark is used for the status bar -->
|
|
||||||
<item name="colorPrimaryDark">#1976D2</item>
|
|
||||||
<!-- colorAccent is used as the default value for colorControlActivated
|
|
||||||
which is used to tint widgets -->
|
|
||||||
<item name="colorAccent">#FF4081</item>
|
|
||||||
<!-- You can also set colorControlNormal, colorControlActivated
|
|
||||||
colorControlHighlight and colorSwitchThumbNormal. -->
|
|
||||||
<item name="windowActionModeOverlay">true</item>
|
|
||||||
|
|
||||||
<item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppCompatDialogStyle" parent="Theme.AppCompat.Light.Dialog">
|
<!-- Main app theme — extends MAUI's generated base so all MAUI internals work correctly -->
|
||||||
<item name="colorAccent">#FF4081</item>
|
<style name="MainTheme" parent="Maui.MainTheme.NoActionBar">
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
<!-- colorOnPrimary controls text/icon color on primary-colored surfaces (toolbar, overflow icon) -->
|
||||||
|
<item name="colorOnPrimary">#ffffff</item>
|
||||||
|
<!-- windowBackground prevents white flash during page transitions -->
|
||||||
|
<item name="android:windowBackground">@color/windowBackground</item>
|
||||||
|
<item name="android:statusBarColor">@color/statusBarColor</item>
|
||||||
|
<item name="actionModeBackground">@color/colorPrimary</item>
|
||||||
|
<item name="actionOverflowButtonStyle">@style/OverflowMenuButton</item>
|
||||||
|
<item name="actionModeStyle">@style/ActionModeStyle</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="OverflowMenuButton" parent="Widget.AppCompat.ActionButton.Overflow">
|
||||||
|
<item name="android:tint">@android:color/white</item>
|
||||||
|
<item name="tint">@android:color/white</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="ActionModeStyle" parent="Widget.AppCompat.ActionMode">
|
||||||
|
<item name="titleTextStyle">@style/ActionModeTitleText</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="ActionModeTitleText" parent="TextAppearance.AppCompat.Widget.ActionMode.Title">
|
||||||
|
<item name="android:textColor">@android:color/white</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// NativeTrustManager.cs
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j.Android
|
||||||
|
//
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Java.Security;
|
||||||
|
using Java.Security.Cert;
|
||||||
|
using Javax.Net.Ssl;
|
||||||
|
using Neo4j.Driver;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j
|
||||||
|
{
|
||||||
|
public class NativeTrustManager : TrustManager
|
||||||
|
{
|
||||||
|
private readonly IX509TrustManager _androidTrustManager;
|
||||||
|
|
||||||
|
public NativeTrustManager()
|
||||||
|
{
|
||||||
|
var factory = TrustManagerFactory.GetInstance(TrustManagerFactory.DefaultAlgorithm);
|
||||||
|
factory.Init((KeyStore)null);
|
||||||
|
_androidTrustManager = factory.GetTrustManagers()
|
||||||
|
.OfType<IX509TrustManager>()
|
||||||
|
.First();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool ValidateServerCertificate(Uri uri, X509Certificate2 certificate, X509Chain chain,
|
||||||
|
SslPolicyErrors sslPolicyErrors)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// The Xamarin binding of CertificateFactory.GenerateCertificate accepts a System.IO.Stream
|
||||||
|
var certFactory = CertificateFactory.GetInstance("X.509");
|
||||||
|
using var stream = new MemoryStream(certificate.RawData);
|
||||||
|
var javaCert = certFactory.GenerateCertificate(stream) as Java.Security.Cert.X509Certificate;
|
||||||
|
|
||||||
|
_androidTrustManager.CheckServerTrusted(
|
||||||
|
new Java.Security.Cert.X509Certificate[] { javaCert }, "RSA");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Certificate validation failed for {uri.Host}: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// ScreenSizeService.cs
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j.Android
|
||||||
|
//
|
||||||
|
|
||||||
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Android.Services
|
||||||
|
{
|
||||||
|
public class ScreenSizeService : IScreenSizeService
|
||||||
|
{
|
||||||
|
public int GetScreenHeight()
|
||||||
|
{
|
||||||
|
var metrics = global::Android.App.Application.Context.Resources.DisplayMetrics;
|
||||||
|
return (int)(metrics.HeightPixels / metrics.Density);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetScreenWidth()
|
||||||
|
{
|
||||||
|
var metrics = global::Android.App.Application.Context.Resources.DisplayMetrics;
|
||||||
|
return (int)(metrics.WidthPixels / metrics.Density);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// TrustManagerService.cs
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j.Android
|
||||||
|
//
|
||||||
|
|
||||||
|
using Neo4j.Driver;
|
||||||
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Android.Services
|
||||||
|
{
|
||||||
|
public class TrustManagerService : ITrustManagerService
|
||||||
|
{
|
||||||
|
public TrustManager GetNativeTrustManager()
|
||||||
|
{
|
||||||
|
return new NativeTrustManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// VersionService.cs
|
||||||
|
//
|
||||||
|
// © Xamarin.Neo4j.Android
|
||||||
|
//
|
||||||
|
|
||||||
|
using Android.OS;
|
||||||
|
using Xamarin.Neo4j.Services.Interfaces;
|
||||||
|
|
||||||
|
namespace Xamarin.Neo4j.Android.Services
|
||||||
|
{
|
||||||
|
public class VersionService : IVersionService
|
||||||
|
{
|
||||||
|
public string GetVersion()
|
||||||
|
{
|
||||||
|
var context = global::Android.App.Application.Context;
|
||||||
|
var info = context.PackageManager.GetPackageInfo(context.PackageName, 0);
|
||||||
|
return info.VersionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetBuild()
|
||||||
|
{
|
||||||
|
var context = global::Android.App.Application.Context;
|
||||||
|
var info = context.PackageManager.GetPackageInfo(context.PackageName, 0);
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
return Build.VERSION.SdkInt >= BuildVersionCodes.P
|
||||||
|
? info.LongVersionCode.ToString()
|
||||||
|
: info.VersionCode.ToString();
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +1,50 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<TargetFramework>net10.0-android</TargetFramework>
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
<OutputType>Exe</OutputType>
|
||||||
<ProjectGuid>{24C5C58E-C59D-4621-9BF0-2E3BAC547455}</ProjectGuid>
|
<UseMaui>true</UseMaui>
|
||||||
<ProjectTypeGuids>{EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
|
||||||
<OutputType>Library</OutputType>
|
|
||||||
<RootNamespace>Xamarin.Neo4j.Android</RootNamespace>
|
|
||||||
<AssemblyName>Xamarin.Neo4j.Android</AssemblyName>
|
<AssemblyName>Xamarin.Neo4j.Android</AssemblyName>
|
||||||
<AndroidApplication>True</AndroidApplication>
|
<ApplicationId>nl.resoftware.pocketgraph</ApplicationId>
|
||||||
<AndroidResgenFile>Resources\Resource.designer.cs</AndroidResgenFile>
|
<ApplicationTitle>PocketGraph</ApplicationTitle>
|
||||||
<AndroidResgenClass>Resource</AndroidResgenClass>
|
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
|
||||||
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
|
<LangVersion>latest</LangVersion>
|
||||||
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
<Nullable>disable</Nullable>
|
||||||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
<!-- Embed all assemblies in the APK so plain adb install works without Fast Deployment -->
|
||||||
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>portable</DebugType>
|
|
||||||
<Optimize>false</Optimize>
|
|
||||||
<OutputPath>bin\Debug</OutputPath>
|
|
||||||
<DefineConstants>DEBUG;</DefineConstants>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<AndroidLinkMode>None</AndroidLinkMode>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release</OutputPath>
|
|
||||||
<ErrorReport>prompt</ErrorReport>
|
|
||||||
<WarningLevel>4</WarningLevel>
|
|
||||||
<AndroidManagedSymbols>true</AndroidManagedSymbols>
|
|
||||||
<AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Mono.Android" />
|
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.20" />
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2196" />
|
<!-- Link font assets from the iOS Resources folder so MAUI can bundle them for Android -->
|
||||||
|
<AndroidAsset Include="..\Xamarin.Neo4j.iOS\Resources\iconize-fontawesome-brands.ttf"
|
||||||
|
Link="Assets\iconize-fontawesome-brands.ttf" />
|
||||||
|
<AndroidAsset Include="..\Xamarin.Neo4j.iOS\Resources\iconize-fontawesome-regular.ttf"
|
||||||
|
Link="Assets\iconize-fontawesome-regular.ttf" />
|
||||||
|
<AndroidAsset Include="..\Xamarin.Neo4j.iOS\Resources\iconize-fontawesome-solid.ttf"
|
||||||
|
Link="Assets\iconize-fontawesome-solid.ttf" />
|
||||||
|
<AndroidAsset Include="..\Xamarin.Neo4j.iOS\Resources\roboto-mono-regular.ttf"
|
||||||
|
Link="Assets\roboto-mono-regular.ttf" />
|
||||||
|
<AndroidAsset Include="..\Xamarin.Neo4j.iOS\Resources\logo.png"
|
||||||
|
Link="Assets\logo.png" />
|
||||||
|
<AndroidAsset Include="..\Xamarin.Neo4j.iOS\Resources\resoftware.png"
|
||||||
|
Link="Assets\resoftware.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="MainActivity.cs" />
|
<!-- Exclude old Xamarin.Forms layout resources not needed by MAUI -->
|
||||||
<Compile Include="Resources\Resource.Designer.cs" />
|
<AndroidResource Remove="Resources\layout\Tabbar.axml" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<AndroidResource Remove="Resources\layout\Toolbar.axml" />
|
||||||
|
<!-- Exclude old AssemblyInfo and Resource.designer — SDK-style generates these -->
|
||||||
|
<Compile Remove="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Remove="Resources\Resource.Designer.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Resources\AboutResources.txt" />
|
<ProjectReference Include="..\Xamarin.Neo4j\Xamarin.Neo4j.csproj" />
|
||||||
<None Include="Assets\AboutAssets.txt" />
|
|
||||||
<None Include="Properties\AndroidManifest.xml" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<AndroidResource Include="Resources\layout\Tabbar.axml" />
|
|
||||||
<AndroidResource Include="Resources\layout\Toolbar.axml" />
|
|
||||||
<AndroidResource Include="Resources\values\styles.xml" />
|
|
||||||
<AndroidResource Include="Resources\values\colors.xml" />
|
|
||||||
</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\Android\Xamarin.Android.CSharp.targets" />
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using System;
|
||||||
using Microsoft.Maui.ApplicationModel;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
|
using Microsoft.Maui.Graphics;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Controls.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Xamarin.Neo4j.Pages;
|
using Xamarin.Neo4j.Pages;
|
||||||
@@ -10,15 +12,23 @@ namespace Xamarin.Neo4j
|
|||||||
{
|
{
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
public static event EventHandler ThemeChanged;
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
SetTheme(Current.RequestedTheme);
|
SetTheme(Current.RequestedTheme);
|
||||||
|
RequestedThemeChanged += (s, e) => SetTheme(e.RequestedTheme);
|
||||||
|
|
||||||
MainPage = new NavigationPage(new RootPage());
|
MainPage = new NavigationPage(new ConnectionsPage());
|
||||||
|
|
||||||
|
// SetTheme ran before MainPage was assigned, so apply bar colours now.
|
||||||
|
ApplyNavBarColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateTheme(AppTheme theme) => SetTheme(theme);
|
||||||
|
|
||||||
private void SetTheme(AppTheme theme)
|
private void SetTheme(AppTheme theme)
|
||||||
{
|
{
|
||||||
Resources = theme switch
|
Resources = theme switch
|
||||||
@@ -28,6 +38,20 @@ namespace Xamarin.Neo4j
|
|||||||
|
|
||||||
_ => new LightTheme()
|
_ => new LightTheme()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ApplyNavBarColors();
|
||||||
|
ThemeChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called both from SetTheme and from MainActivity.OnResume so that
|
||||||
|
// theme changes while the app is backgrounded are picked up on return.
|
||||||
|
public void ApplyNavBarColors()
|
||||||
|
{
|
||||||
|
if (MainPage is NavigationPage navPage)
|
||||||
|
{
|
||||||
|
navPage.BarBackgroundColor = Color.FromArgb("#31333b");
|
||||||
|
navPage.BarTextColor = Colors.White;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnStart()
|
protected override void OnStart()
|
||||||
|
|||||||
@@ -5,16 +5,9 @@
|
|||||||
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">
|
||||||
<Grid Padding="15, 10">
|
<Grid Padding="15, 10">
|
||||||
<Grid.ColumnDefinitions>
|
<StackLayout Spacing="2">
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Label Grid.Column="0" x:Name="isActiveIndicator" Text="{x:Static fonts:FontAwesomeSolid.Circle}" FontFamily="{StaticResource FontAwesomeSolid}" VerticalOptions="Center" TextColor="#34C759" Margin="0 ,0, 7.5, 0"/>
|
|
||||||
|
|
||||||
<StackLayout Grid.Column="1" Spacing="2">
|
|
||||||
<Label Text="{Binding Name}" />
|
<Label Text="{Binding Name}" />
|
||||||
<Label Text="{Binding Host}" FontSize="Small" TextColor="{StaticResource SecondaryTextColor}" />
|
<Label Text="{Binding Host}" FontSize="Small" TextColor="{DynamicResource SecondaryTextColor}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ViewCell>
|
</ViewCell>
|
||||||
|
|||||||
@@ -15,21 +15,6 @@ namespace Xamarin.Neo4j.Controls
|
|||||||
public ConnectionCell()
|
public ConnectionCell()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
isActiveIndicator.SetBinding(VisualElement.IsVisibleProperty, new Binding(nameof(IsActive), source: this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Bindable Properties
|
|
||||||
|
|
||||||
public bool IsActive
|
|
||||||
{
|
|
||||||
get => (bool)GetValue(IsActiveProperty);
|
|
||||||
set => SetValue(IsActiveProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BindableProperty IsActiveProperty = BindableProperty.Create("IsActive", typeof(bool),
|
|
||||||
typeof(ConnectionCell), false, BindingMode.OneWay);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ViewCell xmlns="http://schemas.microsoft.com/dotnet/2021/maui" 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="{DynamicResource Accent}" Text="{Binding Name}">
|
||||||
<Label.GestureRecognizers>
|
<Label.GestureRecognizers>
|
||||||
<TapGestureRecognizer Command="{Binding OpenRepo}" />
|
<TapGestureRecognizer Command="{Binding OpenRepo}" />
|
||||||
</Label.GestureRecognizers>
|
</Label.GestureRecognizers>
|
||||||
</Label>
|
</Label>
|
||||||
<ContentView Padding="10" BackgroundColor="#D8D8D8">
|
<ContentView Padding="10" BackgroundColor="{DynamicResource QueryTextBorder}">
|
||||||
<Label Text="{Binding LicenseText}" />
|
<Label Text="{Binding LicenseText}" TextColor="{DynamicResource SecondaryTextColor}" />
|
||||||
</ContentView>
|
</ContentView>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ViewCell>
|
</ViewCell>
|
||||||
|
|||||||
@@ -2,28 +2,28 @@
|
|||||||
|
|
||||||
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<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"
|
x:Class="Xamarin.Neo4j.Controls.QueryResultView"
|
||||||
Margin="0, 0, 0, 25"
|
x:Name="self">
|
||||||
x:Class="Xamarin.Neo4j.Controls.QueryResultView">
|
<Grid VerticalOptions="Start">
|
||||||
<StackLayout Orientation="Vertical" Spacing="0">
|
|
||||||
<Border Stroke="{StaticResource QueryTextBorder}" StrokeThickness="4">
|
<!-- Graph WebView -->
|
||||||
<Label Text="{Binding Query}" Padding="8" HorizontalOptions="FillAndExpand" BackgroundColor="{StaticResource Extreme}" />
|
<WebView x:Name="graphView"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
IsVisible="{Binding CanDisplayGraph}"
|
||||||
|
HeightRequest="{Binding GraphViewHeight, Source={x:Reference self}}" />
|
||||||
|
|
||||||
|
<!-- Error display -->
|
||||||
|
<Border IsVisible="{Binding IsError}"
|
||||||
|
BackgroundColor="{DynamicResource ErrorBackground}"
|
||||||
|
StrokeThickness="0"
|
||||||
|
Padding="12">
|
||||||
|
<StackLayout Spacing="6">
|
||||||
|
<Label Text="{Binding ErrorMessage}"
|
||||||
|
TextColor="{DynamicResource ErrorTextColor}"
|
||||||
|
FontSize="14"
|
||||||
|
FontFamily="RobotoMono" />
|
||||||
|
</StackLayout>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<WebView x:Name="graphView" HorizontalOptions="FillAndExpand" IsVisible="{Binding CanDisplayGraph}" HeightRequest="200" />
|
|
||||||
|
|
||||||
<Grid BackgroundColor="{StaticResource QueryActionBar}" Padding="5">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
<ColumnDefinition Width="1*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Button Grid.Column="0" Clicked="SaveQuery" Text="{x:Static fonts:FontAwesomeSolid.Star}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20" />
|
|
||||||
<Button Grid.Column="1" Clicked="OpenNeovis" Text="{x:Static fonts:FontAwesomeSolid.Expand}" BackgroundColor="Transparent" IsEnabled="{Binding CanDisplayGraph}" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
|
|
||||||
<Button Grid.Column="2" Clicked="OpenTableView" Text="{x:Static fonts:FontAwesomeSolid.List}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
|
|
||||||
<Button Grid.Column="3" Clicked="CloseResultView" Text="{x:Static fonts:FontAwesomeSolid.Times}" BackgroundColor="Transparent" TextColor="White" FontFamily="{StaticResource FontAwesomeSolid}" FontSize="20 "/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackLayout>
|
|
||||||
</ContentView>
|
</ContentView>
|
||||||
|
|||||||
@@ -1,43 +1,49 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Maui.ApplicationModel;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Controls.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Maui;
|
using Microsoft.Maui;
|
||||||
using Neo4j.Driver;
|
|
||||||
using Xamarin.Neo4j.Managers;
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Services;
|
using Xamarin.Neo4j.Services;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
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
|
||||||
{
|
{
|
||||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
public partial class QueryResultView : ContentView
|
public partial class QueryResultView : ContentView
|
||||||
{
|
{
|
||||||
private QueryResult QueryResult => (QueryResult) BindingContext;
|
public static readonly BindableProperty GraphViewHeightProperty =
|
||||||
|
BindableProperty.Create(nameof(GraphViewHeight), typeof(double), typeof(QueryResultView), 300.0);
|
||||||
|
|
||||||
public event EventHandler<GenericEventArgs<QueryResult>> CloseRequested;
|
public double GraphViewHeight
|
||||||
|
{
|
||||||
|
get => (double)GetValue(GraphViewHeightProperty);
|
||||||
|
set => SetValue(GraphViewHeightProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
private string _neovisHtml;
|
private QueryResult QueryResult => (QueryResult)BindingContext;
|
||||||
|
|
||||||
public QueryResultView()
|
public QueryResultView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BindingContextChanged += OnBindingContextChanged;
|
BindingContextChanged += OnBindingContextChanged;
|
||||||
graphView.Navigating += OnGraphViewNavigating;
|
graphView.Navigating += OnGraphViewNavigating;
|
||||||
|
App.ThemeChanged += OnThemeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThemeChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (QueryResult == null || graphView == null) return;
|
||||||
|
ParseNeovisHtmlSafe();
|
||||||
|
graphView.Source = new HtmlWebViewSource { Html = QueryResult?.NeovisHtml };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnGraphViewNavigating(object sender, WebNavigatingEventArgs e)
|
private async void OnGraphViewNavigating(object sender, WebNavigatingEventArgs e)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[Graph] Inline Navigating: {e.Url}");
|
|
||||||
|
|
||||||
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
||||||
|
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
@@ -69,19 +75,11 @@ namespace Xamarin.Neo4j.Controls
|
|||||||
|
|
||||||
private void OnBindingContextChanged(object sender, EventArgs e)
|
private void OnBindingContextChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (BindingContext == null || graphView == null)
|
if (BindingContext == null || graphView == null) return;
|
||||||
{
|
|
||||||
Console.WriteLine($"[Graph] OnBindingContextChanged skipped: BindingContext={BindingContext}, graphView={graphView}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"[Graph] OnBindingContextChanged fired, CanDisplayGraph={QueryResult?.CanDisplayGraph}");
|
|
||||||
|
|
||||||
ParseNeovisHtmlSafe();
|
ParseNeovisHtmlSafe();
|
||||||
|
|
||||||
Console.WriteLine($"[Graph] Setting graphView.Source, html length={_neovisHtml?.Length ?? 0}");
|
graphView.Source = new HtmlWebViewSource { Html = QueryResult?.NeovisHtml };
|
||||||
|
|
||||||
graphView.Source = new HtmlWebViewSource { Html = _neovisHtml };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseNeovisHtml()
|
private void ParseNeovisHtml()
|
||||||
@@ -89,83 +87,46 @@ namespace Xamarin.Neo4j.Controls
|
|||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
var resourceName = "Xamarin.Neo4j.Visualization.visgraph.html";
|
var resourceName = "Xamarin.Neo4j.Visualization.visgraph.html";
|
||||||
|
|
||||||
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);
|
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
{
|
{
|
||||||
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
var available = string.Join(", ", assembly.GetManifestResourceNames());
|
||||||
|
QueryResult.NeovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||||
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
|
$"<b>Resource not found:</b><br>{resourceName}<br><br>" +
|
||||||
$"<b>Available:</b><br>{available.Replace("\n", "<br>")}</body></html>";
|
$"<b>Available:</b><br>{available}</body></html>";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (stream)
|
using (stream)
|
||||||
using (var reader = new StreamReader(stream))
|
using (var reader = new StreamReader(stream))
|
||||||
{
|
{
|
||||||
var result = reader.ReadToEnd();
|
var html = reader.ReadToEnd();
|
||||||
var connectionId = ConnectionStringManager.ActiveConnectionString?.Id ?? Guid.Empty;
|
var connectionId = ConnectionStringManager.ActiveConnectionString?.Id ?? Guid.Empty;
|
||||||
|
|
||||||
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(QueryResult.Results, connectionId);
|
var (nodesJson, edgesJson) = GraphDataHelper.BuildJson(QueryResult.Results, connectionId);
|
||||||
|
|
||||||
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
||||||
result = result.Replace("{{nodes}}", nodesJson);
|
html = html.Replace("{{nodes}}", nodesJson);
|
||||||
result = result.Replace("{{edges}}", edgesJson);
|
html = html.Replace("{{edges}}", edgesJson);
|
||||||
result = result.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
|
html = html.Replace("{{backgroundColor}}", isDark ? "#292C31" : "#FFFFFF");
|
||||||
result = result.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
|
html = html.Replace("{{textColor}}", isDark ? "#FFFFFF" : "#000000");
|
||||||
|
|
||||||
_neovisHtml = result;
|
QueryResult.NeovisHtml = html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseNeovisHtmlSafe()
|
private void ParseNeovisHtmlSafe()
|
||||||
{
|
{
|
||||||
|
if (QueryResult == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ParseNeovisHtml();
|
ParseNeovisHtml();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[Graph] ParseNeovisHtml threw: {ex.GetType().Name}: {ex.Message}");
|
QueryResult.NeovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
||||||
_neovisHtml = $"<html><body style='background:#111;color:#f55;font-family:monospace;padding:16px'>" +
|
$"<b>{ex.GetType().Name}</b><br>{ex.Message}</body></html>";
|
||||||
$"<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)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
var name = await Application.Current.MainPage.DisplayPromptAsync("Save Query", "How should the query be called?");
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
var query = new Query()
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
QueryText = QueryResult.Query,
|
|
||||||
Name = name,
|
|
||||||
};
|
|
||||||
|
|
||||||
SavedQueryManager.AddSavedQuery(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseResultView(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
CloseRequested?.Invoke(sender, new GenericEventArgs<QueryResult>(QueryResult));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OpenTableView(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
await Application.Current.MainPage.Navigation.PushAsync(new TablePage(QueryResult));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,5 +31,10 @@ namespace Xamarin.Neo4j.Models
|
|||||||
public Neo4jConnectionString ConnectionString { get; set; }
|
public Neo4jConnectionString ConnectionString { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, List<object>> Results { get; set; }
|
public Dictionary<string, List<object>> Results { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Cached NeoVis HTML generated by QueryResultView; set when the result is first rendered.</summary>
|
||||||
|
public string NeovisHtml { get; set; }
|
||||||
|
|
||||||
|
public bool IsError => !Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<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"
|
||||||
Title="Add Connection"
|
Title="Add Connection"
|
||||||
x:Class="Xamarin.Neo4j.Pages.AddConnectionPage">
|
x:Class="Xamarin.Neo4j.Pages.AddConnectionPage">
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
@@ -35,12 +36,24 @@
|
|||||||
|
|
||||||
<StackLayout Spacing="4">
|
<StackLayout Spacing="4">
|
||||||
<Label Text="Password:" FontAttributes="Bold" FontSize="Small" />
|
<Label Text="Password:" FontAttributes="Bold" FontSize="Small" />
|
||||||
<Entry Text="{Binding Password}" IsPassword="True" />
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<Entry Grid.Column="0" x:Name="passwordEntry"
|
||||||
|
Text="{Binding Password}"
|
||||||
|
IsPassword="True" />
|
||||||
|
<Button Grid.Column="1" x:Name="eyeButton"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.Eye}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="16"
|
||||||
|
WidthRequest="44"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Clicked="TogglePasswordVisibility" />
|
||||||
|
</Grid>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|
||||||
<Button Text="Connect" BackgroundColor="#2DB4A8" TextColor="White" Command="{Binding Commands[Connect]}" />
|
<Button Text="Test" BackgroundColor="Transparent" TextColor="{DynamicResource SecondaryTextColor}" BorderColor="{DynamicResource SecondaryTextColor}" BorderWidth="1" Command="{Binding Commands[Test]}" />
|
||||||
<Button Text="Test" BackgroundColor="Orange" TextColor="White" Command="{Binding Commands[Test]}" />
|
<Button Text="Connect" BackgroundColor="Transparent" TextColor="{DynamicResource PrimaryTextColor}" BorderColor="{DynamicResource PrimaryTextColor}" BorderWidth="1" Command="{Binding Commands[Connect]}" />
|
||||||
<Button Text="Save" BackgroundColor="#89972E" TextColor="White" Command="{Binding Commands[Save]}" />
|
<Button Text="Save" BackgroundColor="{DynamicResource Accent}" TextColor="{DynamicResource QueryTextBackground}" Command="{Binding Commands[Save]}" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Controls.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
using Xamarin.Neo4j.Fonts;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
@@ -14,11 +15,20 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
public partial class AddConnectionPage : ContentPage
|
public partial class AddConnectionPage : ContentPage
|
||||||
{
|
{
|
||||||
|
private bool _passwordVisible = false;
|
||||||
|
|
||||||
public AddConnectionPage(Neo4jConnectionString neo4JConnectionString = null)
|
public AddConnectionPage(Neo4jConnectionString neo4JConnectionString = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
BindingContext = new AddConnectionViewModel(Navigation, neo4JConnectionString);
|
BindingContext = new AddConnectionViewModel(Navigation, neo4JConnectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TogglePasswordVisibility(object sender, System.EventArgs e)
|
||||||
|
{
|
||||||
|
_passwordVisible = !_passwordVisible;
|
||||||
|
passwordEntry.IsPassword = !_passwordVisible;
|
||||||
|
eyeButton.Text = _passwordVisible ? FontAwesomeSolid.EyeSlash : FontAwesomeSolid.Eye;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,47 +4,20 @@
|
|||||||
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:converters="clr-namespace:Xamarin.Neo4j.Converters;assembly=Xamarin.Neo4j"
|
x:Class="Xamarin.Neo4j.Pages.ConnectionsPage"
|
||||||
x:Class="Xamarin.Neo4j.Pages.ConnectionsPage"
|
|
||||||
x:Name="connectionsPage"
|
x:Name="connectionsPage"
|
||||||
Title="Connections">
|
Title="PocketGraph">
|
||||||
<ContentPage.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<converters:IsActiveConnectionConverter x:Key="isActiveConnection" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</ContentPage.Resources>
|
|
||||||
|
|
||||||
<ContentPage.IconImageSource>
|
|
||||||
<FontImageSource
|
|
||||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
|
||||||
Glyph="{x:Static fonts:FontAwesomeSolid.Server}"
|
|
||||||
Size="20" />
|
|
||||||
</ContentPage.IconImageSource>
|
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Command="{Binding Commands[StartSession]}">
|
<ToolbarItem Text="Settings" Order="Secondary" Command="{Binding Commands[OpenSettings]}" />
|
||||||
<ToolbarItem.IconImageSource>
|
|
||||||
<FontImageSource
|
|
||||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
|
||||||
Glyph="{x:Static fonts:FontAwesomeSolid.Terminal}" Size="20" />
|
|
||||||
</ToolbarItem.IconImageSource>
|
|
||||||
</ToolbarItem>
|
|
||||||
|
|
||||||
<ToolbarItem Command="{Binding Commands[AddConnection]}">
|
|
||||||
<ToolbarItem.IconImageSource>
|
|
||||||
<FontImageSource
|
|
||||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
|
||||||
Glyph="{x:Static fonts:FontAwesomeSolid.PlusCircle}" Size="20" />
|
|
||||||
</ToolbarItem.IconImageSource>
|
|
||||||
</ToolbarItem>
|
|
||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ListView ItemsSource="{Binding ConnectionStrings}" HasUnevenRows="True" SelectionMode="None" ItemTapped="SetActive" IsVisible="{Binding HasItems}">
|
<ListView ItemsSource="{Binding ConnectionStrings}" HasUnevenRows="True" SelectionMode="None" ItemTapped="OpenSession" IsVisible="{Binding HasItems}">
|
||||||
<ListView.ItemTemplate>
|
<ListView.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<controls:ConnectionCell IsActive="{Binding ., Converter={StaticResource isActiveConnection}}">
|
<controls:ConnectionCell>
|
||||||
<controls:ConnectionCell.ContextActions>
|
<controls:ConnectionCell.ContextActions>
|
||||||
<MenuItem Text="Edit" Command="{Binding BindingContext.Commands[EditConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" />
|
<MenuItem Text="Edit" Command="{Binding BindingContext.Commands[EditConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" />
|
||||||
<MenuItem Text="Delete" Command="{Binding BindingContext.Commands[DeleteConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" IsDestructive="true" />
|
<MenuItem Text="Delete" Command="{Binding BindingContext.Commands[DeleteConnectionString], Source={x:Reference connectionsPage}}" CommandParameter="{Binding .}" IsDestructive="true" />
|
||||||
@@ -58,18 +31,30 @@
|
|||||||
<Label Text="{x:Static fonts:FontAwesomeSolid.Server}"
|
<Label Text="{x:Static fonts:FontAwesomeSolid.Server}"
|
||||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
FontSize="48"
|
FontSize="48"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="No connections"
|
<Label Text="No connections"
|
||||||
FontSize="18"
|
FontSize="18"
|
||||||
FontAttributes="Bold"
|
FontAttributes="Bold"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="Tap + to add a connection"
|
<Label Text="Tap + to add a connection"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
|
||||||
|
<!-- FAB -->
|
||||||
|
<Button Text="{x:Static fonts:FontAwesomeSolid.Plus}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="20"
|
||||||
|
WidthRequest="56" HeightRequest="56"
|
||||||
|
CornerRadius="28"
|
||||||
|
BackgroundColor="{DynamicResource Accent}"
|
||||||
|
TextColor="White"
|
||||||
|
VerticalOptions="End" HorizontalOptions="End"
|
||||||
|
Margin="0,0,16,16"
|
||||||
|
Command="{Binding Commands[AddConnection]}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -26,14 +26,25 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
|
|
||||||
protected override void OnAppearing()
|
protected override void OnAppearing()
|
||||||
{
|
{
|
||||||
|
App.ThemeChanged += OnThemeChanged;
|
||||||
ViewModel.LoadConnectionStrings();
|
ViewModel.LoadConnectionStrings();
|
||||||
|
|
||||||
base.OnAppearing();
|
base.OnAppearing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetActive(object sender, ItemTappedEventArgs e)
|
protected override void OnDisappearing()
|
||||||
{
|
{
|
||||||
ViewModel.SetActiveConnectionString((Neo4jConnectionString)e.Item);
|
App.ThemeChanged -= OnThemeChanged;
|
||||||
|
base.OnDisappearing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThemeChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.LoadConnectionStrings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenSession(object sender, ItemTappedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.OpenSession((Neo4jConnectionString)e.Item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,25 @@
|
|||||||
Title="Graph"
|
Title="Graph"
|
||||||
x:Class="Xamarin.Neo4j.Pages.GraphPage">
|
x:Class="Xamarin.Neo4j.Pages.GraphPage">
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<WebView x:Name="webView" Source="{Binding Source}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />
|
<Grid>
|
||||||
|
<WebView x:Name="webView" Source="{Binding Source}"
|
||||||
|
HorizontalOptions="FillAndExpand"
|
||||||
|
VerticalOptions="FillAndExpand" />
|
||||||
|
|
||||||
|
<!-- Double-tap hint — hidden after first interaction -->
|
||||||
|
<Border x:Name="hintBorder"
|
||||||
|
VerticalOptions="End"
|
||||||
|
HorizontalOptions="Center"
|
||||||
|
Margin="16,0,16,16"
|
||||||
|
BackgroundColor="#CC000000"
|
||||||
|
StrokeThickness="0"
|
||||||
|
StrokeShape="RoundRectangle 12"
|
||||||
|
Padding="12,8">
|
||||||
|
<Label Text="Double-tap a node to expand its relationships"
|
||||||
|
TextColor="White"
|
||||||
|
FontSize="13"
|
||||||
|
HorizontalTextAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
{
|
{
|
||||||
Console.WriteLine($"[Graph] Navigating: {e.Url}");
|
Console.WriteLine($"[Graph] Navigating: {e.Url}");
|
||||||
|
|
||||||
|
if (e.Url.Contains("interaction"))
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
hintBorder.IsVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
if (!e.Url.Contains("expand") || !e.Url.Contains("nodeId")) return;
|
||||||
|
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
|
|||||||
@@ -31,16 +31,16 @@
|
|||||||
<Label Text="{x:Static fonts:FontAwesomeSolid.Code}"
|
<Label Text="{x:Static fonts:FontAwesomeSolid.Code}"
|
||||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
FontSize="48"
|
FontSize="48"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="No saved queries"
|
<Label Text="No saved queries"
|
||||||
FontSize="18"
|
FontSize="18"
|
||||||
FontAttributes="Bold"
|
FontAttributes="Bold"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="Save a query from a session to see it here"
|
<Label Text="Save a query from a session to see it here"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
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"
|
||||||
x:Class="Xamarin.Neo4j.Pages.SessionPage">
|
x:Class="Xamarin.Neo4j.Pages.SessionPage"
|
||||||
|
x:Name="sessionPage">
|
||||||
<NavigationPage.TitleView>
|
<NavigationPage.TitleView>
|
||||||
<StackLayout Spacing="0">
|
<StackLayout Spacing="0">
|
||||||
<Label TextColor="White" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
|
<Label TextColor="White" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand">
|
||||||
@@ -35,40 +36,210 @@
|
|||||||
</ContentPage.ToolbarItems>
|
</ContentPage.ToolbarItems>
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<StackLayout VerticalOptions="FillAndExpand">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<Border BackgroundColor="White" StrokeShape="RoundRectangle 2" Padding="4" Margin="16">
|
|
||||||
<Border Stroke="#d8e5f1" StrokeThickness="2" StrokeShape="RoundRectangle 2">
|
<!-- Row 0: Query editor -->
|
||||||
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery" FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
|
<Border Grid.Row="0"
|
||||||
</Border>
|
BackgroundColor="{DynamicResource QueryTextBackground}"
|
||||||
|
Stroke="{DynamicResource QueryTextBorder}"
|
||||||
|
StrokeThickness="1"
|
||||||
|
StrokeShape="RoundRectangle 2"
|
||||||
|
Padding="4"
|
||||||
|
Margin="16">
|
||||||
|
<controls:QueryEditor Text="{Binding Query}" ExecuteClicked="ExecuteQuery"
|
||||||
|
Placeholder="MATCH (n) RETURN n LIMIT 25"
|
||||||
|
PlaceholderColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
BackgroundColor="{DynamicResource QueryTextBackground}"
|
||||||
|
TextColor="{DynamicResource PrimaryTextColor}"
|
||||||
|
FontSize="14" MaxHeight="200" HorizontalOptions="FillAndExpand" AutoSize="TextChanges" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid VerticalOptions="FillAndExpand">
|
<!-- Row 1: Single scroll area — results then saved queries always visible -->
|
||||||
<CollectionView x:Name="resultsCollection" ItemsSource="{Binding QueryResults}" VerticalOptions="FillAndExpand">
|
<ScrollView Grid.Row="1" x:Name="mainScroll">
|
||||||
<CollectionView.ItemTemplate>
|
<StackLayout Spacing="0" Padding="0,0,0,16">
|
||||||
|
|
||||||
|
<!-- Saved queries section — collapsible, always above results -->
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,10,16,4">
|
||||||
|
<Grid.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Tapped="ToggleSavedQueries" />
|
||||||
|
</Grid.GestureRecognizers>
|
||||||
|
<Label Grid.Column="0"
|
||||||
|
Text="Saved Queries"
|
||||||
|
FontSize="12" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
x:Name="savedQueriesChevron"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronUp}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="11"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackLayout x:Name="savedQueriesContent" Spacing="0" Margin="16,0">
|
||||||
|
<StackLayout BindableLayout.ItemsSource="{Binding SavedQueries}"
|
||||||
|
IsVisible="{Binding HasSavedQueries}"
|
||||||
|
Spacing="0">
|
||||||
|
<BindableLayout.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<controls:QueryResultView CloseRequested="CloseResultView" />
|
<Grid ColumnDefinitions="*,Auto" Padding="0,10">
|
||||||
|
<StackLayout Grid.Column="0" Spacing="2">
|
||||||
|
<StackLayout.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer
|
||||||
|
Command="{Binding BindingContext.Commands[LoadQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
</StackLayout.GestureRecognizers>
|
||||||
|
<Label Text="{Binding Name}" FontSize="14" FontAttributes="Bold" />
|
||||||
|
<Label Text="{Binding QueryText}"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
MaxLines="1"
|
||||||
|
LineBreakMode="TailTruncation" />
|
||||||
|
</StackLayout>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.TrashAlt}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="16"
|
||||||
|
WidthRequest="44"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Command="{Binding BindingContext.Commands[DeleteQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</CollectionView.ItemTemplate>
|
</BindableLayout.ItemTemplate>
|
||||||
</CollectionView>
|
</StackLayout>
|
||||||
|
|
||||||
<StackLayout IsVisible="{Binding IsEmpty}" VerticalOptions="Center" HorizontalOptions="Center" Spacing="12" Padding="32">
|
<Label Text="No saved queries yet"
|
||||||
|
IsVisible="{Binding HasNoSavedQueries}"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
FontSize="14"
|
||||||
|
Margin="0,4,0,8" />
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<!-- Divider between saved queries and results -->
|
||||||
|
<BoxView HeightRequest="1"
|
||||||
|
BackgroundColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Opacity="0.15"
|
||||||
|
Margin="16,8,16,0" />
|
||||||
|
|
||||||
|
<!-- Results section -->
|
||||||
|
<StackLayout IsVisible="{Binding HasResults}" Spacing="0">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,8,16,4">
|
||||||
|
<Label Grid.Column="0"
|
||||||
|
Text="Results"
|
||||||
|
FontSize="12" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Text="Clear all"
|
||||||
|
FontSize="11"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Padding="8,0"
|
||||||
|
HeightRequest="32"
|
||||||
|
Command="{Binding Commands[ClearResults]}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackLayout BindableLayout.ItemsSource="{Binding QueryResults}" Spacing="0">
|
||||||
|
<BindableLayout.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Margin="12,0,12,8"
|
||||||
|
BackgroundColor="{DynamicResource Extreme}"
|
||||||
|
Stroke="{DynamicResource QueryTextBorder}"
|
||||||
|
StrokeThickness="1"
|
||||||
|
StrokeShape="RoundRectangle 4">
|
||||||
|
<StackLayout Spacing="0">
|
||||||
|
<!-- Action row -->
|
||||||
|
<Grid ColumnDefinitions="*,*,*,*,Auto" Padding="4,2">
|
||||||
|
<Button Grid.Column="0"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.Star}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[SaveQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.Expand}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
IsEnabled="{Binding CanDisplayGraph}"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[OpenGraph], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="2"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.Code}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
IsEnabled="{Binding Success}"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[OpenTable], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="3"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.TrashAlt}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="15"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
HeightRequest="40"
|
||||||
|
Command="{Binding BindingContext.Commands[DeleteResult], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
<Button Grid.Column="4"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronUp}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}" FontSize="12"
|
||||||
|
BackgroundColor="Transparent"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
WidthRequest="40" HeightRequest="40"
|
||||||
|
Clicked="ToggleResultCollapse" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Query label -->
|
||||||
|
<Label Text="{Binding DisplayQuery}"
|
||||||
|
Padding="12,0,12,8"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
MaxLines="2">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer
|
||||||
|
Command="{Binding BindingContext.Commands[LoadResultQuery], Source={x:Reference sessionPage}}"
|
||||||
|
CommandParameter="{Binding .}" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<!-- Result content -->
|
||||||
|
<controls:QueryResultView
|
||||||
|
GraphViewHeight="{Binding BindingContext.GraphViewHeight, Source={x:Reference sessionPage}}" />
|
||||||
|
</StackLayout>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</BindableLayout.ItemTemplate>
|
||||||
|
</StackLayout>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<!-- Empty hint (no results yet) -->
|
||||||
|
<StackLayout IsVisible="{Binding IsEmpty}" HorizontalOptions="Center" Spacing="12" Padding="32,24">
|
||||||
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
|
<Label Text="{x:Static fonts:FontAwesomeSolid.Database}"
|
||||||
FontFamily="{StaticResource FontAwesomeSolid}"
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
FontSize="48"
|
FontSize="48"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="No results yet"
|
<Label Text="No results yet"
|
||||||
FontSize="18"
|
FontSize="18" FontAttributes="Bold"
|
||||||
FontAttributes="Bold"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
<Label Text="Write a query and tap the play button to run it"
|
<Label Text="Write a query and tap play, or load a saved query above"
|
||||||
FontSize="14"
|
FontSize="14"
|
||||||
TextColor="{StaticResource SecondaryTextColor}"
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
HorizontalTextAlignment="Center"
|
HorizontalTextAlignment="Center"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</Grid>
|
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Controls.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
using Xamarin.Neo4j.Fonts;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Utilities;
|
|
||||||
using Xamarin.Neo4j.ViewModels;
|
using Xamarin.Neo4j.ViewModels;
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Pages
|
namespace Xamarin.Neo4j.Pages
|
||||||
@@ -10,7 +10,7 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||||
public partial class SessionPage : ContentPage
|
public partial class SessionPage : ContentPage
|
||||||
{
|
{
|
||||||
private SessionViewModel ViewModel => (SessionViewModel) BindingContext;
|
private SessionViewModel ViewModel => (SessionViewModel)BindingContext;
|
||||||
|
|
||||||
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
|
public SessionPage(Neo4jConnectionString connectionString, string initialQuery = null)
|
||||||
{
|
{
|
||||||
@@ -18,25 +18,58 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
|
|
||||||
BindingContext = new SessionViewModel(Navigation, connectionString, initialQuery);
|
BindingContext = new SessionViewModel(Navigation, connectionString, initialQuery);
|
||||||
|
|
||||||
ViewModel.ScrollToTop += (_, _) =>
|
ViewModel.ScrollToTop += async (_, _) =>
|
||||||
{
|
{
|
||||||
resultsCollection.ScrollTo(0, 0, ScrollToPosition.Start, true);
|
await mainScroll.ScrollToAsync(0, 0, true);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
ViewModel.LoadSavedQueries();
|
||||||
|
}
|
||||||
|
|
||||||
private void FocusDatabasePicker(object sender, EventArgs e)
|
private void FocusDatabasePicker(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
databasePicker.Focus();
|
databasePicker.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CloseResultView(object sender, GenericEventArgs<QueryResult> e)
|
|
||||||
{
|
|
||||||
ViewModel.DeleteQueryResult(e.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExecuteQuery(object sender, EventArgs e)
|
private void ExecuteQuery(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
ViewModel.Commands["ExecuteQuery"].Execute(null);
|
ViewModel.Commands["ExecuteQuery"].Execute(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _savedQueriesExpanded = true;
|
||||||
|
|
||||||
|
private void ToggleSavedQueries(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_savedQueriesExpanded = !_savedQueriesExpanded;
|
||||||
|
savedQueriesContent.IsVisible = _savedQueriesExpanded;
|
||||||
|
savedQueriesChevron.Text = _savedQueriesExpanded
|
||||||
|
? FontAwesomeSolid.ChevronUp
|
||||||
|
: FontAwesomeSolid.ChevronDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleResultCollapse(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Button btn) return;
|
||||||
|
|
||||||
|
// Walk up to find the StackLayout that wraps action row + query + content
|
||||||
|
var card = btn.Parent?.Parent as StackLayout; // btn -> Grid -> StackLayout
|
||||||
|
if (card == null) return;
|
||||||
|
|
||||||
|
// The collapsible content is the last child (QueryResultView)
|
||||||
|
var content = card.Children[card.Children.Count - 1] as View;
|
||||||
|
// The query label is second-to-last
|
||||||
|
var queryLabel = card.Children.Count >= 2 ? card.Children[card.Children.Count - 2] as View : null;
|
||||||
|
|
||||||
|
if (content == null) return;
|
||||||
|
|
||||||
|
var collapse = content.IsVisible;
|
||||||
|
content.IsVisible = !collapse;
|
||||||
|
if (queryLabel != null) queryLabel.IsVisible = !collapse;
|
||||||
|
btn.Text = collapse ? FontAwesomeSolid.ChevronDown : FontAwesomeSolid.ChevronUp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,33 +14,156 @@
|
|||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<Grid RowDefinitions="*,Auto">
|
<Grid RowDefinitions="*,Auto">
|
||||||
<TableView Grid.Row="0" Intent="Settings" Background="Transparent" HasUnevenRows="True">
|
|
||||||
<TableRoot>
|
|
||||||
<TableSection Title="Support">
|
|
||||||
<TextCell Text="Contact Support" Command="{Binding Commands[ContactSupport]}" />
|
|
||||||
<TextCell Text="Rate PocketGraph" Command="{Binding Commands[RateApp]}" />
|
|
||||||
</TableSection>
|
|
||||||
|
|
||||||
<TableSection Title="Data">
|
<ScrollView Grid.Row="0">
|
||||||
<TextCell Text="Clear Saved Connections" Command="{Binding Commands[ClearConnections]}" />
|
<StackLayout Padding="16" Spacing="24">
|
||||||
<TextCell Text="Clear Saved Queries" Command="{Binding Commands[ClearQueries]}" />
|
|
||||||
</TableSection>
|
|
||||||
|
|
||||||
<TableSection Title="About">
|
<!-- Support -->
|
||||||
<TextCell Text="{Binding VersionLabel}" />
|
<StackLayout Spacing="8">
|
||||||
<TextCell Text="Software Licenses" Command="{Binding Commands[OpenLicensesPage]}" />
|
<Label Text="SUPPORT"
|
||||||
</TableSection>
|
FontSize="11" FontAttributes="Bold"
|
||||||
</TableRoot>
|
CharacterSpacing="2"
|
||||||
</TableView>
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Margin="4,0,0,0" />
|
||||||
<StackLayout Grid.Row="1" Spacing="15" Padding="25">
|
<Border BackgroundColor="{DynamicResource Extreme}"
|
||||||
<Label Text="A product of" HorizontalOptions="Center" FontAttributes="Italic" FontSize="12" TextColor="{StaticResource SecondaryTextColor}" />
|
Stroke="{DynamicResource QueryTextBorder}"
|
||||||
<Image Source="resoftware.png" HorizontalOptions="Center" WidthRequest="100">
|
StrokeThickness="1"
|
||||||
<Image.GestureRecognizers>
|
StrokeShape="RoundRectangle 4">
|
||||||
<TapGestureRecognizer Command="{Binding Commands[OpenReSoftwareSite]}" />
|
<StackLayout Spacing="0">
|
||||||
</Image.GestureRecognizers>
|
<Grid ColumnDefinitions="*,Auto" Padding="16,14">
|
||||||
</Image>
|
<Grid.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding Commands[ContactSupport]}" />
|
||||||
|
</Grid.GestureRecognizers>
|
||||||
|
<Label Grid.Column="0" Text="Contact Support"
|
||||||
|
TextColor="{DynamicResource PrimaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronRight}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
|
<BoxView HeightRequest="1" BackgroundColor="{DynamicResource QueryTextBorder}" Margin="16,0" />
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,14">
|
||||||
|
<Grid.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding Commands[RateApp]}" />
|
||||||
|
</Grid.GestureRecognizers>
|
||||||
|
<Label Grid.Column="0" Text="Rate PocketGraph"
|
||||||
|
TextColor="{DynamicResource PrimaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronRight}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
|
</Border>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<!-- Data -->
|
||||||
|
<StackLayout Spacing="8">
|
||||||
|
<Label Text="DATA"
|
||||||
|
FontSize="11" FontAttributes="Bold"
|
||||||
|
CharacterSpacing="2"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Margin="4,0,0,0" />
|
||||||
|
<Border BackgroundColor="{DynamicResource Extreme}"
|
||||||
|
Stroke="{DynamicResource QueryTextBorder}"
|
||||||
|
StrokeThickness="1"
|
||||||
|
StrokeShape="RoundRectangle 4">
|
||||||
|
<StackLayout Spacing="0">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,14">
|
||||||
|
<Grid.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding Commands[ClearConnections]}" />
|
||||||
|
</Grid.GestureRecognizers>
|
||||||
|
<Label Grid.Column="0" Text="Clear Saved Connections"
|
||||||
|
TextColor="{DynamicResource PrimaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronRight}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
|
<BoxView HeightRequest="1" BackgroundColor="{DynamicResource QueryTextBorder}" Margin="16,0" />
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,14">
|
||||||
|
<Grid.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding Commands[ClearQueries]}" />
|
||||||
|
</Grid.GestureRecognizers>
|
||||||
|
<Label Grid.Column="0" Text="Clear Saved Queries"
|
||||||
|
TextColor="{DynamicResource PrimaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronRight}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackLayout>
|
||||||
|
</Border>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
<!-- About -->
|
||||||
|
<StackLayout Spacing="8">
|
||||||
|
<Label Text="ABOUT"
|
||||||
|
FontSize="11" FontAttributes="Bold"
|
||||||
|
CharacterSpacing="2"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
Margin="4,0,0,0" />
|
||||||
|
<Border BackgroundColor="{DynamicResource Extreme}"
|
||||||
|
Stroke="{DynamicResource QueryTextBorder}"
|
||||||
|
StrokeThickness="1"
|
||||||
|
StrokeShape="RoundRectangle 4">
|
||||||
|
<StackLayout Spacing="0">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,14">
|
||||||
|
<Label Grid.Column="0" Text="{Binding VersionLabel}"
|
||||||
|
TextColor="{DynamicResource PrimaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
|
<BoxView HeightRequest="1" BackgroundColor="{DynamicResource QueryTextBorder}" Margin="16,0" />
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Padding="16,14">
|
||||||
|
<Grid.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding Commands[OpenLicensesPage]}" />
|
||||||
|
</Grid.GestureRecognizers>
|
||||||
|
<Label Grid.Column="0" Text="Software Licenses"
|
||||||
|
TextColor="{DynamicResource PrimaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
<Label Grid.Column="1"
|
||||||
|
Text="{x:Static fonts:FontAwesomeSolid.ChevronRight}"
|
||||||
|
FontFamily="{StaticResource FontAwesomeSolid}"
|
||||||
|
FontSize="12"
|
||||||
|
TextColor="{DynamicResource SecondaryTextColor}"
|
||||||
|
VerticalOptions="Center" />
|
||||||
|
</Grid>
|
||||||
|
</StackLayout>
|
||||||
|
</Border>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<StackLayout Grid.Row="1" Spacing="4" Padding="25,16">
|
||||||
|
<Label HorizontalOptions="Center" FontSize="12">
|
||||||
|
<Label.GestureRecognizers>
|
||||||
|
<TapGestureRecognizer Command="{Binding Commands[OpenReSoftwareSite]}" />
|
||||||
|
</Label.GestureRecognizers>
|
||||||
|
<Label.FormattedText>
|
||||||
|
<FormattedString>
|
||||||
|
<Span Text="A product of " TextColor="{DynamicResource SecondaryTextColor}" FontAttributes="Italic" />
|
||||||
|
<Span Text="Re: Software" TextColor="{DynamicResource Accent}" FontAttributes="Bold" />
|
||||||
|
</FormattedString>
|
||||||
|
</Label.FormattedText>
|
||||||
|
</Label>
|
||||||
|
</StackLayout>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -2,31 +2,11 @@
|
|||||||
|
|
||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<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"
|
Title="JSON View"
|
||||||
Title="{Binding Title}"
|
|
||||||
x:Class="Xamarin.Neo4j.Pages.TablePage">
|
x:Class="Xamarin.Neo4j.Pages.TablePage">
|
||||||
<ContentPage.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<converters:JsonConvertConverter x:Key="jsonConvert" />
|
|
||||||
</ResourceDictionary>
|
|
||||||
</ContentPage.Resources>
|
|
||||||
|
|
||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<CarouselView ItemsSource="{Binding Rows}" Position="{Binding Position}" HorizontalScrollBarVisibility="Never" VerticalScrollBarVisibility="Never">
|
<WebView x:Name="jsonWebView"
|
||||||
<CarouselView.ItemTemplate>
|
HorizontalOptions="FillAndExpand"
|
||||||
<DataTemplate>
|
VerticalOptions="FillAndExpand" />
|
||||||
<CollectionView ItemsSource="{Binding .}">
|
|
||||||
<CollectionView.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<StackLayout Margin="0, 0, 0, 25" Padding="10">
|
|
||||||
<Label Text="{Binding Key, StringFormat='{0}:'}" FontAttributes="Bold" />
|
|
||||||
<Label Text="{Binding Value, Converter={StaticResource jsonConvert}}" FontSize="Small" />
|
|
||||||
</StackLayout>
|
|
||||||
</DataTemplate>
|
|
||||||
</CollectionView.ItemTemplate>
|
|
||||||
</CollectionView>
|
|
||||||
</DataTemplate>
|
|
||||||
</CarouselView.ItemTemplate>
|
|
||||||
</CarouselView>
|
|
||||||
</ContentPage.Content>
|
</ContentPage.Content>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.Maui.ApplicationModel;
|
||||||
|
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Controls.Xaml;
|
using Microsoft.Maui.Controls.Xaml;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.ViewModels;
|
|
||||||
|
|
||||||
namespace Xamarin.Neo4j.Pages
|
namespace Xamarin.Neo4j.Pages
|
||||||
{
|
{
|
||||||
@@ -17,8 +17,72 @@ namespace Xamarin.Neo4j.Pages
|
|||||||
public TablePage(QueryResult queryResult)
|
public TablePage(QueryResult queryResult)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
LoadJson(queryResult);
|
||||||
|
}
|
||||||
|
|
||||||
BindingContext = new TableViewModel(Navigation, queryResult);
|
protected override void OnAppearing()
|
||||||
|
{
|
||||||
|
base.OnAppearing();
|
||||||
|
App.ThemeChanged += OnThemeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisappearing()
|
||||||
|
{
|
||||||
|
App.ThemeChanged -= OnThemeChanged;
|
||||||
|
base.OnDisappearing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThemeChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
||||||
|
jsonWebView.EvaluateJavaScriptAsync($"setTheme({(isDark ? "true" : "false")})");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadJson(QueryResult queryResult)
|
||||||
|
{
|
||||||
|
// Transpose column-oriented dict into a list of row objects
|
||||||
|
var results = queryResult.Results;
|
||||||
|
string json;
|
||||||
|
if (results == null || results.Count == 0)
|
||||||
|
{
|
||||||
|
json = "[]";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var columns = results.Keys.ToList();
|
||||||
|
var rowCount = results[columns[0]].Count;
|
||||||
|
var rows = Enumerable.Range(0, rowCount)
|
||||||
|
.Select(i => columns.ToDictionary(c => c, c => results[c][i]))
|
||||||
|
.ToList();
|
||||||
|
json = JsonConvert.SerializeObject(rows, Formatting.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = LoadTemplate(json);
|
||||||
|
jsonWebView.Source = new HtmlWebViewSource { Html = html };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string LoadTemplate(string json)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
const string resourceName = "Xamarin.Neo4j.Visualization.jsonview.html";
|
||||||
|
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
|
if (stream == null)
|
||||||
|
return $"<html><body>Resource not found: {resourceName}</body></html>";
|
||||||
|
|
||||||
|
string html;
|
||||||
|
using (stream)
|
||||||
|
using (var reader = new StreamReader(stream))
|
||||||
|
html = reader.ReadToEnd();
|
||||||
|
|
||||||
|
var isDark = Application.Current.RequestedTheme == AppTheme.Dark;
|
||||||
|
html = html.Replace("{{json}}", json);
|
||||||
|
html = html.Replace("{{backgroundColor}}", isDark ? "#1e1e1e" : "#ffffff");
|
||||||
|
html = html.Replace("{{textColor}}", isDark ? "#d4d4d4" : "#1a1a1a");
|
||||||
|
html = html.Replace("{{toolbarBg}}", isDark ? "#252526" : "#f5f5f5");
|
||||||
|
html = html.Replace("{{inputBg}}", isDark ? "#3c3c3c" : "#ffffff");
|
||||||
|
html = html.Replace("{{borderColor}}", isDark ? "#454545" : "#cccccc");
|
||||||
|
html = html.Replace("{{mutedColor}}", isDark ? "#808080" : "#8a8a8a");
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,18 +51,20 @@ namespace Xamarin.Neo4j.Services
|
|||||||
config.WithTrustManager(_nativeTrustManager);
|
config.WithTrustManager(_nativeTrustManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
GraphClient = new BoltGraphClient(driver);
|
// Verify credentials with a real round-trip — this is the only reliable
|
||||||
|
// way to catch auth failures, since the driver connects lazily.
|
||||||
|
var verifySession = driver.AsyncSession();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await GraphClient.ConnectAsync();
|
var cursor = await verifySession.RunAsync("RETURN 1");
|
||||||
|
await cursor.ConsumeAsync();
|
||||||
}
|
}
|
||||||
catch
|
finally
|
||||||
{
|
{
|
||||||
// ConnectAsync runs Neo4j-specific metadata queries that non-Neo4j
|
await verifySession.CloseAsync();
|
||||||
// servers (e.g. Memgraph) don't support. The underlying driver is
|
|
||||||
// still connected and usable for running queries.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GraphClient = new BoltGraphClient(driver);
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (ServiceUnavailableException e)
|
catch (ServiceUnavailableException e)
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
<ResourceDictionary Source="Fonts.xaml" />
|
<ResourceDictionary Source="Fonts.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
<Color x:Key="PageBackground">#545863</Color>
|
<Color x:Key="PageBackground">#0c0c0c</Color>
|
||||||
<Color x:Key="SecondaryTextColor">#a6a7b2</Color>
|
<Color x:Key="ErrorBackground">#1a0000</Color>
|
||||||
<Color x:Key="Accent">#31333b</Color>
|
<Color x:Key="ErrorTextColor">#ff6b6b</Color>
|
||||||
<Color x:Key="QueryTextBackground">Black</Color>
|
<Color x:Key="PrimaryTextColor">#e2e2e2</Color>
|
||||||
<Color x:Key="QueryTextBorder">#686868</Color>
|
<Color x:Key="SecondaryTextColor">#868686</Color>
|
||||||
<Color x:Key="QueryActionBar">#686868</Color>
|
<Color x:Key="Accent">#e2e2e2</Color>
|
||||||
<Color x:Key="Extreme">Black</Color>
|
<Color x:Key="QueryTextBackground">#141414</Color>
|
||||||
|
<Color x:Key="QueryTextBorder">#2a2a2a</Color>
|
||||||
|
<Color x:Key="QueryActionBar">#141414</Color>
|
||||||
|
<Color x:Key="Extreme">#141414</Color>
|
||||||
|
|
||||||
<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
|
<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
|
||||||
<Setter Property="BackgroundColor" Value="{StaticResource PageBackground}" />
|
<Setter Property="BackgroundColor" Value="{StaticResource PageBackground}" />
|
||||||
@@ -21,22 +24,22 @@
|
|||||||
|
|
||||||
<Style TargetType="ListView">
|
<Style TargetType="ListView">
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||||
<Setter Property="SeparatorColor" Value="LightGray" />
|
<Setter Property="SeparatorColor" Value="#2a2a2a" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="NavigationPage">
|
<Style TargetType="NavigationPage">
|
||||||
<Setter Property="BarBackgroundColor" Value="{StaticResource Accent}"/>
|
<Setter Property="BarBackgroundColor" Value="#141414"/>
|
||||||
<Setter Property="BarTextColor" Value="White" />
|
<Setter Property="BarTextColor" Value="#e2e2e2" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="TabbedPage" ApplyToDerivedTypes="True">
|
<Style TargetType="TabbedPage" ApplyToDerivedTypes="True">
|
||||||
<Setter Property="BackgroundColor" Value="White"/>
|
<Setter Property="BackgroundColor" Value="#141414"/>
|
||||||
<Setter Property="BarTextColor" Value="Black"/>
|
<Setter Property="BarTextColor" Value="#868686"/>
|
||||||
<Setter Property="SelectedTabColor" Value="White"/>
|
<Setter Property="SelectedTabColor" Value="#e2e2e2"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="Editor" ApplyToDerivedTypes="True">
|
<Style TargetType="Editor" ApplyToDerivedTypes="True">
|
||||||
<Setter Property="BackgroundColor" Value="White"/>
|
<Setter Property="BackgroundColor" Value="#141414"/>
|
||||||
<Setter Property="TextColor" Value="Black"/>
|
<Setter Property="TextColor" Value="#e2e2e2"/>
|
||||||
</Style>
|
</Style>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -4,18 +4,8 @@
|
|||||||
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">
|
||||||
|
|
||||||
<OnPlatform x:TypeArguments="x:String" x:Key="FontAwesomeBrands">
|
<x:String x:Key="FontAwesomeBrands">FontAwesome5Brands-Regular</x:String>
|
||||||
<On Platform="Android" Value="FontAwesome5Brands.ttf#Regular" />
|
<x:String x:Key="FontAwesomeSolid">FontAwesome5Free-Solid</x:String>
|
||||||
<On Platform="iOS" Value="FontAwesome5Brands-Regular" />
|
<x:String x:Key="FontAwesomeRegular">FontAwesome5Free-Regular</x:String>
|
||||||
</OnPlatform>
|
|
||||||
|
|
||||||
<OnPlatform x:TypeArguments="x:String" x:Key="FontAwesomeSolid">
|
|
||||||
<On Platform="Android" Value="FontAwesome5Solid.ttf#Regular" />
|
|
||||||
<On Platform="iOS" Value="FontAwesome5Free-Solid" />
|
|
||||||
</OnPlatform>
|
|
||||||
|
|
||||||
<OnPlatform x:TypeArguments="x:String" x:Key="FontAwesomeRegular">
|
|
||||||
<On Platform="Android" Value="FontAwesome5Regular.ttf#Regular" />
|
|
||||||
<On Platform="iOS" Value="FontAwesome5Free-Regular" />
|
|
||||||
</OnPlatform>
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
<ResourceDictionary Source="Fonts.xaml" />
|
<ResourceDictionary Source="Fonts.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
<Color x:Key="PageBackground">#e4ecf5</Color>
|
<Color x:Key="PageBackground">#f5f5f5</Color>
|
||||||
|
<Color x:Key="ErrorBackground">#ffebee</Color>
|
||||||
|
<Color x:Key="ErrorTextColor">#c62828</Color>
|
||||||
|
<Color x:Key="PrimaryTextColor">#0c0c0c</Color>
|
||||||
<Color x:Key="SecondaryTextColor">#868a92</Color>
|
<Color x:Key="SecondaryTextColor">#868a92</Color>
|
||||||
<Color x:Key="Accent">#31333b</Color>
|
<Color x:Key="Accent">#0c0c0c</Color>
|
||||||
<Color x:Key="QueryTextBackground">#e0e0e0</Color>
|
<Color x:Key="QueryTextBackground">#ffffff</Color>
|
||||||
<Color x:Key="QueryTextBorder">#31333b</Color>
|
<Color x:Key="QueryTextBorder">#e0e0e0</Color>
|
||||||
<Color x:Key="QueryActionBar">#31343B</Color>
|
<Color x:Key="QueryActionBar">#f5f5f5</Color>
|
||||||
<Color x:Key="Extreme">#FFFFFF</Color>
|
<Color x:Key="Extreme">#ffffff</Color>
|
||||||
|
|
||||||
<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
|
<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
|
||||||
<Setter Property="BackgroundColor" Value="{StaticResource PageBackground}" />
|
<Setter Property="BackgroundColor" Value="{StaticResource PageBackground}" />
|
||||||
@@ -21,16 +24,31 @@
|
|||||||
|
|
||||||
<Style TargetType="ListView">
|
<Style TargetType="ListView">
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||||
|
<Setter Property="SeparatorColor" Value="#e8e8e8" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Entry" ApplyToDerivedTypes="True">
|
||||||
|
<Setter Property="TextColor" Value="#0c0c0c" />
|
||||||
|
<Setter Property="PlaceholderColor" Value="#868a92" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Picker" ApplyToDerivedTypes="True">
|
||||||
|
<Setter Property="TextColor" Value="#0c0c0c" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="NavigationPage">
|
<Style TargetType="NavigationPage">
|
||||||
<Setter Property="BarBackgroundColor" Value="{StaticResource Accent}"/>
|
<Setter Property="BarBackgroundColor" Value="#0c0c0c"/>
|
||||||
<Setter Property="BarTextColor" Value="White" />
|
<Setter Property="BarTextColor" Value="#e2e2e2" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Editor" ApplyToDerivedTypes="True">
|
||||||
|
<Setter Property="BackgroundColor" Value="#ffffff"/>
|
||||||
|
<Setter Property="TextColor" Value="#0c0c0c"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="TabbedPage" ApplyToDerivedTypes="True">
|
<Style TargetType="TabbedPage" ApplyToDerivedTypes="True">
|
||||||
<Setter Property="BackgroundColor" Value="White"/>
|
<Setter Property="BackgroundColor" Value="#ffffff"/>
|
||||||
<Setter Property="BarTextColor" Value="Black"/>
|
<Setter Property="BarTextColor" Value="#868a92"/>
|
||||||
<Setter Property="SelectedTabColor" Value="{StaticResource Accent}"/>
|
<Setter Property="SelectedTabColor" Value="#0c0c0c"/>
|
||||||
</Style>
|
</Style>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -50,16 +50,9 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
LoadConnectionStrings();
|
LoadConnectionStrings();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Commands.Add("StartSession", new Command(async () =>
|
Commands.Add("OpenSettings", new Command(async () =>
|
||||||
{
|
{
|
||||||
if (ConnectionStringManager.ActiveConnectionString == null)
|
await Navigation.PushAsync(new SettingsPage());
|
||||||
{
|
|
||||||
await Application.Current.MainPage.DisplayAlert("", "Please select a connection before starting a session.", "OK");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Navigation.PushAsync(new SessionPage(ConnectionStringManager.ActiveConnectionString));
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +67,11 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
ConnectionStrings = await ConnectionStringManager.GetConnectionStrings();
|
ConnectionStrings = await ConnectionStringManager.GetConnectionStrings();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetActiveConnectionString(Neo4jConnectionString connectionString)
|
public async void OpenSession(Neo4jConnectionString connectionString)
|
||||||
{
|
{
|
||||||
ConnectionStringManager.ActiveConnectionString = ConnectionStringManager.ActiveConnectionString?.Id == connectionString.Id ? null : connectionString;
|
ConnectionStringManager.ActiveConnectionString = connectionString;
|
||||||
|
|
||||||
LoadConnectionStrings();
|
LoadConnectionStrings();
|
||||||
|
await Navigation.PushAsync(new SessionPage(connectionString));
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Bindable Properties
|
#region Bindable Properties
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Maui;
|
using Microsoft.Maui;
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Xamarin.Neo4j.Annotations;
|
using Xamarin.Neo4j.Annotations;
|
||||||
|
using Xamarin.Neo4j.Managers;
|
||||||
using Xamarin.Neo4j.Models;
|
using Xamarin.Neo4j.Models;
|
||||||
using Xamarin.Neo4j.Pages;
|
using Xamarin.Neo4j.Pages;
|
||||||
using Xamarin.Neo4j.Services;
|
using Xamarin.Neo4j.Services;
|
||||||
@@ -41,6 +42,8 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
private ObservableCollection<QueryResult> _queryResults;
|
private ObservableCollection<QueryResult> _queryResults;
|
||||||
|
|
||||||
|
private List<Query> _savedQueries;
|
||||||
|
|
||||||
private Neo4jConnectionString _connectionString;
|
private Neo4jConnectionString _connectionString;
|
||||||
|
|
||||||
private string _query;
|
private string _query;
|
||||||
@@ -54,7 +57,11 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
Query = initialQuery;
|
Query = initialQuery;
|
||||||
QueryResults = new ObservableCollection<QueryResult>();
|
QueryResults = new ObservableCollection<QueryResult>();
|
||||||
QueryResults.CollectionChanged += (_, _) => OnPropertyChanged(nameof(IsEmpty));
|
QueryResults.CollectionChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsEmpty));
|
||||||
|
OnPropertyChanged(nameof(HasResults));
|
||||||
|
};
|
||||||
|
|
||||||
Commands.Add("ExecuteQuery", new Command(async () =>
|
Commands.Add("ExecuteQuery", new Command(async () =>
|
||||||
{
|
{
|
||||||
@@ -65,20 +72,91 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
|
|
||||||
var result = await _neo4jService.ExecuteQuery(Query, _connectionString);
|
var result = await _neo4jService.ExecuteQuery(Query, _connectionString);
|
||||||
|
|
||||||
if (result.Success)
|
Query = null;
|
||||||
{
|
|
||||||
QueryResults.Insert(0, result);
|
QueryResults.Insert(0, result);
|
||||||
|
|
||||||
ScrollToTop?.Invoke(this, EventArgs.Empty);
|
ScrollToTop?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
await Application.Current.MainPage.DisplayAlert("", result.ErrorMessage, "OK");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Commands.Add("DeleteQuery", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is Query query))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var confirmed = await Application.Current.MainPage.DisplayAlert(
|
||||||
|
"Delete Query",
|
||||||
|
$"Delete \"{query.Name}\"?",
|
||||||
|
"Delete", "Cancel");
|
||||||
|
|
||||||
|
if (confirmed)
|
||||||
|
{
|
||||||
|
SavedQueryManager.DeleteSavedQuery(query);
|
||||||
|
LoadSavedQueries();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("LoadQuery", new Command((o) =>
|
||||||
|
{
|
||||||
|
if (o is Query query)
|
||||||
|
LoadQuery(query);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("LoadResultQuery", new Command((o) =>
|
||||||
|
{
|
||||||
|
if (o is QueryResult result)
|
||||||
|
Query = result.Query;
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("DeleteResult", new Command((o) =>
|
||||||
|
{
|
||||||
|
if (o is QueryResult result)
|
||||||
|
DeleteQueryResult(result);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("SaveQuery", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is QueryResult result)) return;
|
||||||
|
var name = await Application.Current.MainPage.DisplayPromptAsync(
|
||||||
|
"Save Query", "What would you like to call this query?");
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
SavedQueryManager.AddSavedQuery(new Query
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
QueryText = result.Query,
|
||||||
|
Name = name
|
||||||
|
});
|
||||||
|
LoadSavedQueries();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("OpenGraph", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is QueryResult result) || !result.CanDisplayGraph || result.NeovisHtml == null) return;
|
||||||
|
var connectionString2 = ConnectionStringManager.ActiveConnectionString;
|
||||||
|
await Navigation.PushAsync(new GraphPage(result.NeovisHtml, connectionString2, _neo4jService));
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("OpenTable", new Command(async (o) =>
|
||||||
|
{
|
||||||
|
if (!(o is QueryResult result) || !result.Success) return;
|
||||||
|
await Navigation.PushAsync(new TablePage(result));
|
||||||
|
}));
|
||||||
|
|
||||||
|
Commands.Add("ClearResults", new Command(() => ClearAllResults()));
|
||||||
|
|
||||||
InitializeConnection(connectionString);
|
InitializeConnection(connectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void LoadSavedQueries()
|
||||||
|
{
|
||||||
|
SavedQueries = SavedQueryManager.GetSavedQueries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadQuery(Query query)
|
||||||
|
{
|
||||||
|
Query = query.QueryText;
|
||||||
|
}
|
||||||
|
|
||||||
private async void InitializeConnection(Neo4jConnectionString connectionString)
|
private async void InitializeConnection(Neo4jConnectionString connectionString)
|
||||||
{
|
{
|
||||||
var (isConnected, message) = await _neo4jService.EstablishConnection(connectionString);
|
var (isConnected, message) = await _neo4jService.EstablishConnection(connectionString);
|
||||||
@@ -106,6 +184,11 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
QueryResults.Remove(item);
|
QueryResults.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearAllResults()
|
||||||
|
{
|
||||||
|
QueryResults.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
[NotifyPropertyChangedInvocator]
|
[NotifyPropertyChangedInvocator]
|
||||||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
{
|
{
|
||||||
@@ -164,10 +247,39 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Query> SavedQueries
|
||||||
|
{
|
||||||
|
get => _savedQueries;
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_savedQueries = value;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(HasSavedQueries));
|
||||||
|
OnPropertyChanged(nameof(HasNoSavedQueries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanExecuteQuery => !string.IsNullOrWhiteSpace(Query) && CurrentDatabase != null;
|
public bool CanExecuteQuery => !string.IsNullOrWhiteSpace(Query) && CurrentDatabase != null;
|
||||||
|
|
||||||
public bool IsEmpty => QueryResults?.Count == 0;
|
public bool IsEmpty => QueryResults?.Count == 0;
|
||||||
|
|
||||||
|
public bool HasResults => !IsEmpty;
|
||||||
|
|
||||||
|
public bool HasSavedQueries => _savedQueries?.Count > 0;
|
||||||
|
|
||||||
|
public bool HasNoSavedQueries => !HasSavedQueries;
|
||||||
|
|
||||||
|
public double GraphViewHeight
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var screenHeight = _screenSizeService.GetScreenHeight();
|
||||||
|
return Math.Max(200, screenHeight - 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,13 +75,23 @@ namespace Xamarin.Neo4j.ViewModels
|
|||||||
Body = body
|
Body = body
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!Email.Default.IsComposeSupported)
|
||||||
|
{
|
||||||
|
await Application.Current.MainPage.DisplayAlert(
|
||||||
|
"No Email App", "No email client is configured on this device.", "OK");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Email.Default.ComposeAsync(message);
|
await Email.Default.ComposeAsync(message);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Commands.Add("RateApp", new Command(async () =>
|
Commands.Add("RateApp", new Command(async () =>
|
||||||
{
|
{
|
||||||
await Launcher.Default.OpenAsync(
|
var url = DeviceInfo.Platform == DevicePlatform.Android
|
||||||
new Uri("https://apps.apple.com/app/id1604368926?action=write-review"));
|
? "https://play.google.com/store/apps/details?id=nl.resoftware.pocketgraph"
|
||||||
|
: "https://apps.apple.com/app/id1604368926?action=write-review";
|
||||||
|
|
||||||
|
await Launcher.Default.OpenAsync(new Uri(url));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Commands.Add("ClearConnections", new Command(async () =>
|
Commands.Add("ClearConnections", new Command(async () =>
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
<style>
|
||||||
|
:root{--bg:{{backgroundColor}};--fg:{{textColor}};--toolbar:{{toolbarBg}};--input:{{inputBg}};--border:{{borderColor}};--muted:{{mutedColor}}}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
|
||||||
|
body{font-family:'Courier New',monospace;font-size:13px;background:var(--bg);color:var(--fg);overscroll-behavior:none}
|
||||||
|
#toolbar{position:sticky;top:0;z-index:10;background:var(--toolbar);padding:8px;display:flex;flex-direction:column;gap:6px;border-bottom:1px solid var(--border)}
|
||||||
|
#toolbar input{width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--input);color:var(--fg);font-family:inherit;font-size:13px;outline:none;-webkit-appearance:none}
|
||||||
|
#toolbar-row2{display:flex;gap:6px;align-items:center}
|
||||||
|
#toolbar-row2 input{flex:1}
|
||||||
|
.tbtn{padding:5px 10px;border:1px solid var(--border);border-radius:6px;background:var(--input);color:var(--muted);font-size:11px;cursor:pointer;white-space:nowrap;flex-shrink:0}
|
||||||
|
.tbtn:active{opacity:.7}
|
||||||
|
#status{font-size:11px;color:var(--muted);padding:0 2px;min-height:15px}
|
||||||
|
#output{padding:10px}
|
||||||
|
.s{color:#4CAF50}.n{color:#64B5F6}.b{color:#FFB74D}.nl{color:#90A4AE}.k{color:#EF9A9A}.e{color:#EF5350}
|
||||||
|
.br{color:var(--fg);opacity:.7}.cm{color:var(--fg);opacity:.4}
|
||||||
|
/* Block-based tree nodes */
|
||||||
|
.node,.knode{display:block}
|
||||||
|
.nhd{display:block;cursor:default}
|
||||||
|
.tog{cursor:pointer;user-select:none;color:var(--muted);display:inline-block;width:16px;font-size:11px}
|
||||||
|
.tog:hover{color:var(--fg)}
|
||||||
|
.ch{padding-left:18px;border-left:1px dotted var(--border);margin-left:7px}
|
||||||
|
.node.cls>.ch,.knode.cls>.ch{display:none}
|
||||||
|
.sum{display:none;color:var(--muted)}
|
||||||
|
.node.cls .sum,.knode.cls .sum{display:inline}
|
||||||
|
.row{white-space:pre-wrap;word-break:break-word}
|
||||||
|
.hl{background:#FFF176;color:#000;border-radius:2px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="toolbar">
|
||||||
|
<input id="srch" type="search" placeholder="Search…" />
|
||||||
|
<div id="toolbar-row2">
|
||||||
|
<input id="filt" type="search" placeholder="jq: .key .[0] select(.id==1) map(.name) keys" />
|
||||||
|
<button class="tbtn" onclick="collapseAll(true)">⊟ All</button>
|
||||||
|
<button class="tbtn" onclick="collapseAll(false)">⊞ All</button>
|
||||||
|
</div>
|
||||||
|
<div id="status"></div>
|
||||||
|
</div>
|
||||||
|
<div id="output"></div>
|
||||||
|
<script>
|
||||||
|
const RAW={{json}};
|
||||||
|
let cur=RAW,srch='';
|
||||||
|
|
||||||
|
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||||
|
function hl(s){if(!srch)return esc(s);const re=new RegExp('('+srch.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+')','gi');return esc(s).replace(re,'<span class="hl">$1</span>')}
|
||||||
|
|
||||||
|
let uid=0;
|
||||||
|
|
||||||
|
// Render a primitive value inline
|
||||||
|
function rvPrim(v){
|
||||||
|
if(v===null)return'<span class="nl">null</span>';
|
||||||
|
if(typeof v==='string')return'<span class="s">"'+hl(v)+'"</span>';
|
||||||
|
if(typeof v==='number')return'<span class="n">'+hl(v)+'</span>';
|
||||||
|
if(typeof v==='boolean')return'<span class="b">'+v+'</span>';
|
||||||
|
return esc(String(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render any value. For objects/arrays returns a block div; for primitives returns inline span.
|
||||||
|
function rv(v,cm){
|
||||||
|
const c=cm?'<span class="cm">,</span>':'';
|
||||||
|
if(v===null||typeof v!=='object')return rvPrim(v)+c;
|
||||||
|
if(Array.isArray(v))return ra(v,cm);
|
||||||
|
return ro(v,cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ra(a,cm){
|
||||||
|
const c=cm?'<span class="cm">,</span>':'';
|
||||||
|
if(!a.length)return'<span class="br">[]</span>'+c;
|
||||||
|
const id='n'+(uid++);
|
||||||
|
const items=a.map((v,i)=>rvItem(v,i<a.length-1)).join('');
|
||||||
|
return'<div class="node" id="'+id+'">'+
|
||||||
|
'<div class="nhd"><span class="tog" onclick="tog(\''+id+'\')">▾</span><span class="br">[</span><span class="sum"> '+a.length+' ]'+c+'</span></div>'+
|
||||||
|
'<div class="ch">'+items+'<div class="row"><span class="br">]</span>'+c+'</div></div>'+
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ro(o,cm){
|
||||||
|
const c=cm?'<span class="cm">,</span>':'';
|
||||||
|
const ks=Object.keys(o);
|
||||||
|
if(!ks.length)return'<span class="br">{}</span>'+c;
|
||||||
|
const id='n'+(uid++);
|
||||||
|
const inner=ks.map((k,i)=>rkv(k,o[k],i<ks.length-1)).join('');
|
||||||
|
return'<div class="node" id="'+id+'">'+
|
||||||
|
'<div class="nhd"><span class="tog" onclick="tog(\''+id+'\')">▾</span><span class="br">{</span><span class="sum"> '+ks.length+' }'+c+'</span></div>'+
|
||||||
|
'<div class="ch">'+inner+'<div class="row"><span class="br">}</span>'+c+'</div></div>'+
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a key-value pair inside an object. Integrates key+toggle+bracket on one line for object/array values.
|
||||||
|
function rkv(k,v,hasComma){
|
||||||
|
const c=hasComma?'<span class="cm">,</span>':'';
|
||||||
|
if(v!==null&&typeof v==='object'){
|
||||||
|
const isArr=Array.isArray(v);
|
||||||
|
const ob=isArr?'[':'{', cb=isArr?']':'}';
|
||||||
|
const n=isArr?v.length:Object.keys(v).length;
|
||||||
|
if(!n)return'<div class="row"><span class="k">"'+hl(k)+'"</span><span class="br">: '+ob+cb+'</span>'+c+'</div>';
|
||||||
|
const id='n'+(uid++);
|
||||||
|
const children=isArr
|
||||||
|
?v.map((val,i)=>rvItem(val,i<v.length-1)).join('')
|
||||||
|
:Object.keys(v).map((sk,i)=>rkv(sk,v[sk],i<Object.keys(v).length-1)).join('');
|
||||||
|
return'<div class="knode" id="'+id+'">'+
|
||||||
|
'<div class="row nhd"><span class="k">"'+hl(k)+'"</span><span class="br">: </span><span class="tog" onclick="tog(\''+id+'\')">▾</span><span class="br">'+ob+'</span><span class="sum"> '+n+cb+c+'</span></div>'+
|
||||||
|
'<div class="ch">'+children+'<div class="row"><span class="br">'+cb+'</span>'+c+'</div></div>'+
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
return'<div class="row"><span class="k">"'+hl(k)+'"</span><span class="br">: </span>'+rvPrim(v)+c+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render an array item (object/array or primitive)
|
||||||
|
function rvItem(v,hasComma){
|
||||||
|
const c=hasComma?'<span class="cm">,</span>':'';
|
||||||
|
if(v!==null&&typeof v==='object'){
|
||||||
|
const isArr=Array.isArray(v);
|
||||||
|
const ob=isArr?'[':'{', cb=isArr?']':'}';
|
||||||
|
const n=isArr?v.length:Object.keys(v).length;
|
||||||
|
if(!n)return'<div class="row"><span class="br">'+ob+cb+'</span>'+c+'</div>';
|
||||||
|
const id='n'+(uid++);
|
||||||
|
const children=isArr
|
||||||
|
?v.map((val,i)=>rvItem(val,i<v.length-1)).join('')
|
||||||
|
:Object.keys(v).map((k,i)=>rkv(k,v[k],i<Object.keys(v).length-1)).join('');
|
||||||
|
return'<div class="knode" id="'+id+'">'+
|
||||||
|
'<div class="row nhd"><span class="tog" onclick="tog(\''+id+'\')">▾</span><span class="br">'+ob+'</span><span class="sum"> '+n+cb+c+'</span></div>'+
|
||||||
|
'<div class="ch">'+children+'<div class="row"><span class="br">'+cb+'</span>'+c+'</div></div>'+
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
return'<div class="row">'+rvPrim(v)+c+'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function tog(id){const el=document.getElementById(id);el.classList.toggle('cls');el.querySelector('.tog').textContent=el.classList.contains('cls')?'▸':'▾'}
|
||||||
|
|
||||||
|
function collapseAll(collapse){
|
||||||
|
document.querySelectorAll('.node,.knode').forEach(function(el){
|
||||||
|
if(collapse){el.classList.add('cls');var t=el.querySelector('.tog');if(t)t.textContent='▸';}
|
||||||
|
else{el.classList.remove('cls');var t=el.querySelector('.tog');if(t)t.textContent='▾';}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function status(msg,err){const el=document.getElementById('status');el.textContent=msg;el.style.color=err?'#EF5350':'var(--muted)'}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
uid=0;
|
||||||
|
document.getElementById('output').innerHTML='<div class="row">'+rv(cur,false)+'</div>';
|
||||||
|
status(count(cur)+' values'+(srch?' · "'+srch+'"':''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function count(v){
|
||||||
|
if(v===null||typeof v!=='object')return 1;
|
||||||
|
if(Array.isArray(v))return v.reduce((s,i)=>s+count(i),0);
|
||||||
|
return Object.values(v).reduce((s,i)=>s+count(i),0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTheme(isDark){
|
||||||
|
const r=document.documentElement.style;
|
||||||
|
r.setProperty('--bg',isDark?'#1e1e1e':'#ffffff');
|
||||||
|
r.setProperty('--fg',isDark?'#d4d4d4':'#1a1a1a');
|
||||||
|
r.setProperty('--toolbar',isDark?'#252526':'#f5f5f5');
|
||||||
|
r.setProperty('--input',isDark?'#3c3c3c':'#ffffff');
|
||||||
|
r.setProperty('--border',isDark?'#454545':'#cccccc');
|
||||||
|
r.setProperty('--muted',isDark?'#808080':'#8a8a8a');
|
||||||
|
document.body.style.background=isDark?'#1e1e1e':'#ffffff';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('srch').addEventListener('input',function(){srch=this.value.trim().toLowerCase();render()});
|
||||||
|
|
||||||
|
document.getElementById('filt').addEventListener('input',function(){
|
||||||
|
const ex=this.value.trim();
|
||||||
|
if(!ex){cur=RAW;render();return}
|
||||||
|
try{let r=jq(RAW,ex);cur=r===undefined?null:r;render()}
|
||||||
|
catch(e){status('Error: '+e.message,true)}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- jq engine ----
|
||||||
|
function jq(d,ex){
|
||||||
|
ex=ex.trim();
|
||||||
|
if(!ex||ex==='.')return d;
|
||||||
|
if(ex.includes('|')){const ps=pipes(ex);return ps.reduce((x,p)=>{if(Array.isArray(x))return x.flatMap(i=>{const r=jq(i,p);return Array.isArray(r)?r:[r]});return jq(x,p)},d)}
|
||||||
|
if(ex==='.')return d;
|
||||||
|
if(ex==='.[]'){if(Array.isArray(d))return d;if(d&&typeof d==='object')return Object.values(d);throw new Error('.[] on non-iterable')}
|
||||||
|
if(ex==='keys'){ma(d);return Object.keys(d)}
|
||||||
|
if(ex==='values'){ma(d);return Object.values(d)}
|
||||||
|
if(ex==='length')return Array.isArray(d)?d.length:d&&typeof d==='object'?Object.keys(d).length:typeof d==='string'?d.length:null;
|
||||||
|
if(ex==='type')return Array.isArray(d)?'array':typeof d;
|
||||||
|
if(ex==='reverse'){mo(d);return[...d].reverse()}
|
||||||
|
if(ex==='sort'){mo(d);return[...d].sort((a,b)=>a<b?-1:a>b?1:0)}
|
||||||
|
if(ex==='unique'){mo(d);return[...new Set(d.map(v=>JSON.stringify(v)))].map(s=>JSON.parse(s))}
|
||||||
|
if(ex==='first'){mo(d);return d[0]}
|
||||||
|
if(ex==='last'){mo(d);return d[d.length-1]}
|
||||||
|
if(ex==='flatten'){mo(d);return d.flat(Infinity)}
|
||||||
|
if(ex==='add'){mo(d);if(!d.length)return null;if(typeof d[0]==='number')return d.reduce((s,v)=>s+v,0);if(typeof d[0]==='string')return d.join('');return null}
|
||||||
|
if(ex==='any'){mo(d);return d.some(Boolean)}
|
||||||
|
if(ex==='all'){mo(d);return d.every(Boolean)}
|
||||||
|
if(ex==='to_entries'){ma(d);return Object.entries(d).map(([k,v])=>({key:k,value:v}))}
|
||||||
|
if(ex==='from_entries'){mo(d);const o={};for(const e of d)o[e.key||e.name]=e.value;return o}
|
||||||
|
if(ex==='not')return!d;
|
||||||
|
let m;
|
||||||
|
if(m=ex.match(/^\.(\w+)$/))return d?.[m[1]]??null;
|
||||||
|
if(m=ex.match(/^\.\[(-?\d+)\]$/)){const i=+m[1];return Array.isArray(d)?(i<0?d[d.length+i]:d[i]):null}
|
||||||
|
if(m=ex.match(/^\.\[(-?\d*):(-?\d*)\]$/)){mo(d);return d.slice(m[1]?+m[1]:0,m[2]?+m[2]:d.length)}
|
||||||
|
if(m=ex.match(/^\.(\w+)\[\]$/)){const v=d?.[m[1]];return Array.isArray(v)?v:[v]}
|
||||||
|
if(m=ex.match(/^\.(\w+)\?$/)){try{return d?.[m[1]]??null}catch{return null}}
|
||||||
|
if(m=ex.match(/^select\((.+)\)$/)){const ok=cond(d,m[1]);return ok?d:undefined}
|
||||||
|
if(m=ex.match(/^map\((.+)\)$/)){mo(d);return d.map(i=>jq(i,m[1])).filter(v=>v!==undefined)}
|
||||||
|
if(m=ex.match(/^map_values\((.+)\)$/)){if(Array.isArray(d))return d.map(i=>jq(i,m[1]));if(d&&typeof d==='object'){const o={};for(const k of Object.keys(d))o[k]=jq(d[k],m[1]);return o}}
|
||||||
|
if(m=ex.match(/^with_entries\((.+)\)$/))return jq(jq(d,'to_entries'),`map(${m[1]})|from_entries`);
|
||||||
|
if(m=ex.match(/^has\("(\w+)"\)$/))return m[1] in(d||{});
|
||||||
|
if(m=ex.match(/^has\((\d+)\)$/))return+m[1]<(Array.isArray(d)?d.length:0);
|
||||||
|
if(m=ex.match(/^group_by\(\.(\w+)\)$/)){mo(d);const g={};for(const i of d){const k=String(i?.[m[1]]);(g[k]=g[k]||[]).push(i)}return Object.values(g)}
|
||||||
|
if(m=ex.match(/^unique_by\(\.(\w+)\)$/)){mo(d);const s=new Set(),o=[];for(const i of d){const k=JSON.stringify(i?.[m[1]]);if(!s.has(k)){s.add(k);o.push(i)}}return o}
|
||||||
|
if(m=ex.match(/^sort_by\(\.(\w+)\)$/)){mo(d);const k=m[1];return[...d].sort((a,b)=>a?.[k]<b?.[k]?-1:a?.[k]>b?.[k]?1:0)}
|
||||||
|
if(m=ex.match(/^min_by\(\.(\w+)\)$/)){mo(d);const k=m[1];return d.reduce((x,v)=>x===null||v?.[k]<x?.[k]?v:x,null)}
|
||||||
|
if(m=ex.match(/^max_by\(\.(\w+)\)$/)){mo(d);const k=m[1];return d.reduce((x,v)=>x===null||v?.[k]>x?.[k]?v:x,null)}
|
||||||
|
if(m=ex.match(/^del\(\.(\w+)\)$/)){if(Array.isArray(d))return d.map(i=>{const o={...i};delete o[m[1]];return o});const o={...d};delete o[m[1]];return o}
|
||||||
|
if(m=ex.match(/^limit\((\d+);(.+)\)$/)){const r=jq(d,m[2]);return Array.isArray(r)?r.slice(0,+m[1]):r}
|
||||||
|
if(m=ex.match(/^(\.[\w\[\]:,?]+)\.([\w].*)$/))return jq(jq(d,m[1]),'.'+m[2]);
|
||||||
|
throw new Error('Unknown: '+ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cond(d,c){
|
||||||
|
c=c.trim();
|
||||||
|
if(c.includes(' and ')){const[a,b]=c.split(' and ',2);return cond(d,a)&&cond(d,b)}
|
||||||
|
if(c.includes(' or ')){const[a,b]=c.split(' or ',2);return cond(d,a)||cond(d,b)}
|
||||||
|
if(c.startsWith('not '))return!cond(d,c.slice(4));
|
||||||
|
let m;
|
||||||
|
if(m=c.match(/^\.(\w+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/)){
|
||||||
|
const v=d?.[m[1]];let r;try{r=JSON.parse(m[3])}catch{r=m[3].replace(/^"|"$/g,'')}
|
||||||
|
if(m[2]==='==')return v===r;if(m[2]==='!=')return v!==r;if(m[2]==='>')return v>r;if(m[2]==='<')return v<r;if(m[2]==='>='||m[2]==='≥')return v>=r;if(m[2]==='<='||m[2]==='≤')return v<=r;
|
||||||
|
}
|
||||||
|
if(m=c.match(/^has\("(\w+)"\)$/))return m[1] in(d||{});
|
||||||
|
if(m=c.match(/^\.(\w+)$/))return!!(d?.[m[1]]);
|
||||||
|
throw new Error('Unknown condition: '+c);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pipes(ex){const ps=[];let dep=0,cur='';for(let i=0;i<ex.length;i++){if(ex[i]==='(')dep++;else if(ex[i]===')')dep--;else if(ex[i]==='|'&&dep===0){ps.push(cur.trim());cur='';continue}cur+=ex[i]}ps.push(cur.trim());return ps}
|
||||||
|
function mo(v){if(!Array.isArray(v))throw new Error('Expected array, got '+typeof v)}
|
||||||
|
function ma(v){if(!v||typeof v!=='object'||Array.isArray(v))throw new Error('Expected object, got '+typeof v)}
|
||||||
|
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -440,6 +440,16 @@
|
|||||||
|
|
||||||
// ---- interaction handling (touch + mouse/pointer) ----
|
// ---- interaction handling (touch + mouse/pointer) ----
|
||||||
var ptrStartX, ptrStartY, ptrEndX, ptrEndY, ptrStartTime, dragging = null, panning = false, mouseDown = false;
|
var ptrStartX, ptrStartY, ptrEndX, ptrEndY, ptrStartTime, dragging = null, panning = false, mouseDown = false;
|
||||||
|
var interacted = false;
|
||||||
|
function notifyInteraction() {
|
||||||
|
if (interacted) return;
|
||||||
|
interacted = true;
|
||||||
|
var f = document.createElement('iframe');
|
||||||
|
f.style.display = 'none';
|
||||||
|
f.src = 'app://interaction';
|
||||||
|
document.body.appendChild(f);
|
||||||
|
setTimeout(function () { document.body.removeChild(f); }, 200);
|
||||||
|
}
|
||||||
|
|
||||||
function edgeAt(sx, sy) {
|
function edgeAt(sx, sy) {
|
||||||
var w = toWorld(sx, sy);
|
var w = toWorld(sx, sy);
|
||||||
@@ -477,6 +487,7 @@
|
|||||||
|
|
||||||
function handleStart(e) {
|
function handleStart(e) {
|
||||||
if (e.touches) e.preventDefault();
|
if (e.touches) e.preventDefault();
|
||||||
|
notifyInteraction();
|
||||||
var p = coordFromEvent(e);
|
var p = coordFromEvent(e);
|
||||||
ptrStartX = ptrEndX = p.x;
|
ptrStartX = ptrEndX = p.x;
|
||||||
ptrStartY = ptrEndY = p.y;
|
ptrStartY = ptrEndY = p.y;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0-ios</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<UseMaui>true</UseMaui>
|
<UseMaui>true</UseMaui>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Visualization\visgraph.html" />
|
<None Remove="Visualization\visgraph.html" />
|
||||||
<EmbeddedResource Include="Visualization\visgraph.html" />
|
<EmbeddedResource Include="Visualization\visgraph.html" />
|
||||||
|
<None Remove="Visualization\jsonview.html" />
|
||||||
|
<EmbeddedResource Include="Visualization\jsonview.html" />
|
||||||
<None Remove="Resources\licenses.json" />
|
<None Remove="Resources\licenses.json" />
|
||||||
<EmbeddedResource Include="Resources\licenses.json" />
|
<EmbeddedResource Include="Resources\licenses.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
1
Xamarin.Neo4j/distribution/whatsnew/whatsnew-en-US
Normal file
1
Xamarin.Neo4j/distribution/whatsnew/whatsnew-en-US
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Bug fixes and improvements.
|
||||||
Reference in New Issue
Block a user