Skip to content

Commit 5902e33

Browse files
sharpninjaCopilot
andcommitted
feat: add Import button for speech filter phrases from file
- Add ImportFromFileContent to SettingsViewModel supporting .txt, .json, .yaml/.yml - JSON: parses string array or object with single array property - YAML: parses dash-prefixed list items - Plain text: one phrase per line - Merges with existing phrases (case-insensitive dedup), auto-saves - Add Import button with file picker to both Android and Desktop settings views Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 739a805 commit 5902e33

5 files changed

Lines changed: 196 additions & 2 deletions

File tree

src/McpServerManager.Android/Views/PhoneSettingsView.axaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
2323
MinHeight="200"
2424
VerticalContentAlignment="Top"/>
2525

26-
<!-- Save Button + Status -->
26+
<!-- Save/Revert/Import Buttons + Status -->
2727
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="0,8,0,4">
2828
<Button Content="Save" Command="{Binding SaveFilterWordsCommand}" Padding="14,8" FontSize="21"/>
2929
<Button Content="Revert" Command="{Binding RevertFilterWordsCommand}" Padding="14,8" FontSize="21"/>
30+
<Button Content="Import" x:Name="ImportButton" Click="OnImportClick" Padding="14,8" FontSize="21"/>
3031
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Opacity="0.7" FontSize="19"/>
3132
</StackPanel>
3233

src/McpServerManager.Android/Views/PhoneSettingsView.axaml.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
14
using Avalonia.Controls;
5+
using Avalonia.Interactivity;
6+
using Avalonia.Platform.Storage;
7+
using McpServerManager.Core.ViewModels;
28

39
namespace McpServerManager.Android.Views;
410

@@ -8,4 +14,39 @@ public PhoneSettingsView()
814
{
915
InitializeComponent();
1016
}
17+
18+
private async void OnImportClick(object? sender, RoutedEventArgs e)
19+
{
20+
var topLevel = TopLevel.GetTopLevel(this);
21+
if (topLevel?.StorageProvider is not { } storage) return;
22+
23+
var files = await storage.OpenFilePickerAsync(new FilePickerOpenOptions
24+
{
25+
Title = "Import Filter Phrases",
26+
AllowMultiple = false,
27+
FileTypeFilter =
28+
[
29+
new FilePickerFileType("Text files") { Patterns = ["*.txt"] },
30+
new FilePickerFileType("JSON files") { Patterns = ["*.json"] },
31+
new FilePickerFileType("YAML files") { Patterns = ["*.yaml", "*.yml"] },
32+
new FilePickerFileType("All files") { Patterns = ["*"] }
33+
]
34+
});
35+
36+
var file = files.FirstOrDefault();
37+
if (file is null) return;
38+
39+
try
40+
{
41+
await using var stream = await file.OpenReadAsync();
42+
using var reader = new StreamReader(stream);
43+
var content = await reader.ReadToEndAsync();
44+
(DataContext as SettingsViewModel)?.ImportFromFileContent(content, file.Name);
45+
}
46+
catch (Exception ex)
47+
{
48+
if (DataContext is SettingsViewModel vm)
49+
vm.StatusMessage = $"Import error: {ex.Message}";
50+
}
51+
}
1152
}

src/McpServerManager.Core/ViewModels/SettingsViewModel.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Text.Json;
16
using CommunityToolkit.Mvvm.ComponentModel;
27
using CommunityToolkit.Mvvm.Input;
38
using McpServerManager.Core.Services;
@@ -35,4 +40,109 @@ private void RevertFilterWords()
3540
SpeechFilterWords = _speechFilter.FilterText;
3641
StatusMessage = "Reverted to saved values.";
3742
}
43+
44+
/// <summary>
45+
/// Imports phrases from file content. Supports plain text (one per line),
46+
/// JSON (string array), and YAML (dash-prefixed list).
47+
/// </summary>
48+
public void ImportFromFileContent(string content, string fileName)
49+
{
50+
if (string.IsNullOrWhiteSpace(content))
51+
{
52+
StatusMessage = "Import file was empty.";
53+
return;
54+
}
55+
56+
var ext = Path.GetExtension(fileName).ToLowerInvariant();
57+
List<string> phrases;
58+
59+
try
60+
{
61+
phrases = ext switch
62+
{
63+
".json" => ParseJsonPhrases(content),
64+
".yaml" or ".yml" => ParseYamlPhrases(content),
65+
_ => ParsePlainTextPhrases(content)
66+
};
67+
}
68+
catch (Exception ex)
69+
{
70+
StatusMessage = $"Import failed: {ex.Message}";
71+
return;
72+
}
73+
74+
if (phrases.Count == 0)
75+
{
76+
StatusMessage = "No phrases found in file.";
77+
return;
78+
}
79+
80+
// Merge with existing (deduplicate, case-insensitive)
81+
var existing = _speechFilter.FilterText
82+
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
83+
.Select(l => l.Trim())
84+
.Where(l => l.Length > 0)
85+
.ToList();
86+
87+
var merged = existing
88+
.Concat(phrases)
89+
.Distinct(StringComparer.OrdinalIgnoreCase)
90+
.ToList();
91+
92+
var newCount = merged.Count - existing.Count;
93+
SpeechFilterWords = string.Join(Environment.NewLine, merged);
94+
_speechFilter.FilterText = SpeechFilterWords;
95+
StatusMessage = $"Imported {newCount} new phrase(s) ({merged.Count} total). Saved.";
96+
}
97+
98+
private static List<string> ParseJsonPhrases(string content)
99+
{
100+
var doc = JsonDocument.Parse(content);
101+
var root = doc.RootElement;
102+
103+
if (root.ValueKind == JsonValueKind.Array)
104+
{
105+
return root.EnumerateArray()
106+
.Where(e => e.ValueKind == JsonValueKind.String)
107+
.Select(e => e.GetString()!.Trim())
108+
.Where(s => s.Length > 0)
109+
.ToList();
110+
}
111+
112+
// Try object with a single array property
113+
foreach (var prop in root.EnumerateObject())
114+
{
115+
if (prop.Value.ValueKind == JsonValueKind.Array)
116+
{
117+
return prop.Value.EnumerateArray()
118+
.Where(e => e.ValueKind == JsonValueKind.String)
119+
.Select(e => e.GetString()!.Trim())
120+
.Where(s => s.Length > 0)
121+
.ToList();
122+
}
123+
}
124+
125+
throw new InvalidOperationException("JSON must contain a string array.");
126+
}
127+
128+
private static List<string> ParseYamlPhrases(string content)
129+
{
130+
// Simple YAML list parser: lines starting with "- "
131+
return content
132+
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
133+
.Select(l => l.Trim())
134+
.Where(l => l.StartsWith("- ", StringComparison.Ordinal))
135+
.Select(l => l[2..].Trim().Trim('"', '\''))
136+
.Where(s => s.Length > 0)
137+
.ToList();
138+
}
139+
140+
private static List<string> ParsePlainTextPhrases(string content)
141+
{
142+
return content
143+
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
144+
.Select(l => l.Trim())
145+
.Where(l => l.Length > 0)
146+
.ToList();
147+
}
38148
}

src/McpServerManager.Desktop/Views/SettingsView.axaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@
3030
MinHeight="200"
3131
VerticalContentAlignment="Top"/>
3232

33-
<!-- Save Button + Status -->
33+
<!-- Save/Revert/Import Buttons + Status -->
3434
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="0,8,0,0">
3535
<Button Content="Save" Command="{Binding SaveFilterWordsCommand}" Padding="12,6"/>
3636
<Button Content="Revert" Command="{Binding RevertFilterWordsCommand}" Padding="12,6"/>
37+
<Button Content="Import" x:Name="ImportButton" Click="OnImportClick" Padding="12,6"/>
3738
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Opacity="0.7"/>
3839
</StackPanel>
3940

src/McpServerManager.Desktop/Views/SettingsView.axaml.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
14
using Avalonia.Controls;
5+
using Avalonia.Interactivity;
6+
using Avalonia.Platform.Storage;
7+
using McpServerManager.Core.ViewModels;
28

39
namespace McpServerManager.Desktop.Views;
410

@@ -8,4 +14,39 @@ public SettingsView()
814
{
915
InitializeComponent();
1016
}
17+
18+
private async void OnImportClick(object? sender, RoutedEventArgs e)
19+
{
20+
var topLevel = TopLevel.GetTopLevel(this);
21+
if (topLevel?.StorageProvider is not { } storage) return;
22+
23+
var files = await storage.OpenFilePickerAsync(new FilePickerOpenOptions
24+
{
25+
Title = "Import Filter Phrases",
26+
AllowMultiple = false,
27+
FileTypeFilter =
28+
[
29+
new FilePickerFileType("Text files") { Patterns = ["*.txt"] },
30+
new FilePickerFileType("JSON files") { Patterns = ["*.json"] },
31+
new FilePickerFileType("YAML files") { Patterns = ["*.yaml", "*.yml"] },
32+
new FilePickerFileType("All files") { Patterns = ["*"] }
33+
]
34+
});
35+
36+
var file = files.FirstOrDefault();
37+
if (file is null) return;
38+
39+
try
40+
{
41+
await using var stream = await file.OpenReadAsync();
42+
using var reader = new StreamReader(stream);
43+
var content = await reader.ReadToEndAsync();
44+
(DataContext as SettingsViewModel)?.ImportFromFileContent(content, file.Name);
45+
}
46+
catch (Exception ex)
47+
{
48+
if (DataContext is SettingsViewModel vm)
49+
vm.StatusMessage = $"Import error: {ex.Message}";
50+
}
51+
}
1152
}

0 commit comments

Comments
 (0)