diff --git a/JL.Core/Config/CoreConfigManager.cs b/JL.Core/Config/CoreConfigManager.cs index c2bfd530..147cce53 100644 --- a/JL.Core/Config/CoreConfigManager.cs +++ b/JL.Core/Config/CoreConfigManager.cs @@ -15,6 +15,7 @@ public sealed class CoreConfigManager public bool AnkiIntegration { get; set; } // = false; public bool ForceSyncAnki { get; private set; } // = false; public bool AllowDuplicateCards { get; private set; } // = false; + public bool CheckForDuplicateCards { get; private set; } // = false; public double LookupRate { get; private set; } // = 0; public bool CaptureTextFromClipboard { get; set; } = true; public bool CaptureTextFromWebSocket { get; set; } // = false; @@ -95,6 +96,7 @@ public void ApplyPreferences(SqliteConnection connection) AnkiIntegration = ConfigDBManager.GetValueFromConfig(connection, AnkiIntegration, nameof(AnkiIntegration), bool.TryParse); ForceSyncAnki = ConfigDBManager.GetValueFromConfig(connection, ForceSyncAnki, nameof(ForceSyncAnki), bool.TryParse); AllowDuplicateCards = ConfigDBManager.GetValueFromConfig(connection, AllowDuplicateCards, nameof(AllowDuplicateCards), bool.TryParse); + CheckForDuplicateCards = ConfigDBManager.GetValueFromConfig(connection, CheckForDuplicateCards, nameof(CheckForDuplicateCards), bool.TryParse); LookupRate = ConfigDBManager.GetNumberWithDecimalPointFromConfig(connection, LookupRate, nameof(LookupRate), double.TryParse); TextBoxTrimWhiteSpaceCharacters = ConfigDBManager.GetValueFromConfig(connection, TextBoxTrimWhiteSpaceCharacters, nameof(TextBoxTrimWhiteSpaceCharacters), bool.TryParse); TextBoxRemoveNewlines = ConfigDBManager.GetValueFromConfig(connection, TextBoxRemoveNewlines, nameof(TextBoxRemoveNewlines), bool.TryParse); diff --git a/JL.Core/Mining/Anki/AnkiConnect.cs b/JL.Core/Mining/Anki/AnkiConnect.cs index 0fce8407..25f67032 100644 --- a/JL.Core/Mining/Anki/AnkiConnect.cs +++ b/JL.Core/Mining/Anki/AnkiConnect.cs @@ -42,15 +42,12 @@ internal static class AnkiConnect return Send(req); } - public static ValueTask GetCanAddNotesResponse(Note note) + public static ValueTask GetCanAddNotesResponse(List notes) { Request req = new("canAddNotes", 6, new Dictionary(1, StringComparer.Ordinal) { { - "notes", new[] - { - note - } + "notes", notes } }); diff --git a/JL.Core/Mining/Anki/AnkiUtils.cs b/JL.Core/Mining/Anki/AnkiUtils.cs index 20dc88e6..8765450f 100644 --- a/JL.Core/Mining/Anki/AnkiUtils.cs +++ b/JL.Core/Mining/Anki/AnkiUtils.cs @@ -37,11 +37,21 @@ public static class AnkiUtils internal static async Task CanAddNote(Note note) { - Response? response = await AnkiConnect.GetCanAddNotesResponse(note).ConfigureAwait(false); + Response? response = await AnkiConnect.GetCanAddNotesResponse([note]).ConfigureAwait(false); string? resultString = response?.Result?.ToString() ?? null; return resultString is not null ? JsonSerializer.Deserialize>(resultString)![0] : null; } + + internal static async ValueTask?> CanAddNotes(List notes) + { + Response? response = await AnkiConnect.GetCanAddNotesResponse(notes).ConfigureAwait(false); + string? resultString = response?.Result?.ToString() ?? null; + + return resultString is not null + ? JsonSerializer.Deserialize>(resultString) + : null; + } } diff --git a/JL.Core/Mining/MiningUtils.cs b/JL.Core/Mining/MiningUtils.cs index efdb3433..39f0f320 100644 --- a/JL.Core/Mining/MiningUtils.cs +++ b/JL.Core/Mining/MiningUtils.cs @@ -74,6 +74,248 @@ public static class MiningUtils """; + private static string? GetMiningParameter(JLField field, LookupResult lookupResult, string currentText, string? formattedDefinitions, string? selectedDefinitions, int currentCharPosition) + { + switch (field) + { + case JLField.Nothing: + case JLField.Audio: + case JLField.Image: + case JLField.LocalTime: + default: + return null; + case JLField.LeadingSentencePart: + { + string sentence = JapaneseUtils.FindSentence(currentText, currentCharPosition); + int searchStartIndex = currentCharPosition + lookupResult.MatchedText.Length - sentence.Length; + if (searchStartIndex < 0 || searchStartIndex >= currentText.Length) + { + searchStartIndex = 0; + } + + int sentenceStartIndex = currentText.IndexOf(sentence, searchStartIndex, StringComparison.Ordinal); + return currentText[sentenceStartIndex..currentCharPosition]; + } + case JLField.TrailingSentencePart: + { + string sentence = JapaneseUtils.FindSentence(currentText, currentCharPosition); + int searchStartIndex = currentCharPosition + lookupResult.MatchedText.Length - sentence.Length; + if (searchStartIndex < 0 || searchStartIndex >= currentText.Length) + { + searchStartIndex = 0; + } + + int sentenceStartIndex = currentText.IndexOf(sentence, searchStartIndex, StringComparison.Ordinal); + return currentText[(lookupResult.MatchedText.Length + currentCharPosition)..(sentenceStartIndex + sentence.Length)]; + } + case JLField.Sentence: + { + string sentence = JapaneseUtils.FindSentence(currentText, currentCharPosition); + int searchStartIndex = currentCharPosition + lookupResult.MatchedText.Length - sentence.Length; + if (searchStartIndex < 0 || searchStartIndex >= currentText.Length) + { + searchStartIndex = 0; + } + + int sentenceStartIndex = currentText.IndexOf(sentence, searchStartIndex, StringComparison.Ordinal); + string leadingSentencePart = currentText[sentenceStartIndex..currentCharPosition]; + string trailingSentencePart = currentText[(lookupResult.MatchedText.Length + currentCharPosition)..(sentenceStartIndex + sentence.Length)]; + return $"{leadingSentencePart}{lookupResult.MatchedText}{trailingSentencePart}"; + } + case JLField.SourceText: + { + string leadingSourcePart = currentText[..currentCharPosition].ReplaceLineEndings("
"); + string trailingSourcePart = currentText[(currentCharPosition + lookupResult.MatchedText.Length)..].ReplaceLineEndings("
"); + return $"{leadingSourcePart}{lookupResult.MatchedText}{trailingSourcePart}".ReplaceLineEndings("
"); + } + case JLField.Readings: + return lookupResult.Readings is not null ? string.Join('、', lookupResult.Readings) : null; + case JLField.ReadingsWithOrthographyInfo: + return lookupResult.ReadingsOrthographyInfoList is not null && lookupResult.Readings is not null + ? LookupResultUtils.ElementWithOrthographyInfoToText(lookupResult.Readings, lookupResult.ReadingsOrthographyInfoList) + : lookupResult.Readings is not null ? string.Join('、', lookupResult.Readings) : null; + case JLField.FirstReading: + return lookupResult.Readings?[0]; + case JLField.PrimarySpellingAndReadings: + return lookupResult.Readings is not null ? $"{lookupResult.PrimarySpelling}[{string.Join('、', lookupResult.Readings)}]" : null; + case JLField.PrimarySpellingAndFirstReading: + return lookupResult.Readings is not null + ? $"{lookupResult.PrimarySpelling}[{lookupResult.Readings[0]}]" + : null; + case JLField.PrimarySpellingWithOrthographyInfo: + return lookupResult.PrimarySpellingOrthographyInfoList is not null + ? $"{lookupResult.PrimarySpelling} ({string.Join(", ", lookupResult.PrimarySpellingOrthographyInfoList)})" + : lookupResult.PrimarySpelling; + case JLField.AlternativeSpellings: + return lookupResult.AlternativeSpellings is not null ? string.Join('、', lookupResult.AlternativeSpellings) : null; + case JLField.AlternativeSpellingsWithOrthographyInfo: + return lookupResult.AlternativeSpellings is not null ? lookupResult.AlternativeSpellingsOrthographyInfoList is not null + ? LookupResultUtils.ElementWithOrthographyInfoToText(lookupResult.AlternativeSpellings, lookupResult.AlternativeSpellingsOrthographyInfoList) + : string.Join('、', lookupResult.AlternativeSpellings) : null; + case JLField.MatchedText: + return lookupResult.MatchedText; + case JLField.PrimarySpelling: + return lookupResult.PrimarySpelling; + case JLField.DeconjugatedMatchedText: + return lookupResult.DeconjugatedMatchedText; + case JLField.KanjiStats: + return lookupResult.KanjiStats; + case JLField.OnReadings: + return lookupResult.OnReadings is not null ? string.Join('、', lookupResult.OnReadings) : null; + case JLField.KunReadings: + return lookupResult.KunReadings is not null ? string.Join('、', lookupResult.KunReadings) : null; + case JLField.NanoriReadings: + return lookupResult.NanoriReadings is not null ? string.Join('、', lookupResult.NanoriReadings) : null; + case JLField.EdictId: + return lookupResult.EdictId > 0 + ? lookupResult.EdictId.ToString(CultureInfo.InvariantCulture) + : null; + + case JLField.DeconjugationProcess: + return lookupResult.DeconjugationProcess; + case JLField.KanjiComposition: + return lookupResult.KanjiComposition; + case JLField.StrokeCount: + return lookupResult.StrokeCount > 0 + ? lookupResult.StrokeCount.ToString(CultureInfo.InvariantCulture) + : null; + case JLField.KanjiGrade: + return lookupResult.KanjiGrade != byte.MaxValue + ? lookupResult.KanjiGrade.ToString(CultureInfo.InvariantCulture) + : null; + case JLField.RadicalNames: + return lookupResult.RadicalNames is not null + ? string.Join('、', lookupResult.RadicalNames) + : null; + case JLField.Definitions: + return formattedDefinitions?.ReplaceLineEndings("
"); + case JLField.SelectedDefinitions: + return selectedDefinitions is not null ? selectedDefinitions.ReplaceLineEndings("
") : formattedDefinitions?.ReplaceLineEndings("
"); + case JLField.LeadingSourceTextPart: + return currentText[..currentCharPosition].ReplaceLineEndings("
"); + case JLField.TrailingSourceTextPart: + return currentText[(currentCharPosition + lookupResult.MatchedText.Length)..].ReplaceLineEndings("
"); + case JLField.DictionaryName: + return lookupResult.Dict.Name; + case JLField.Frequencies: + return lookupResult.Frequencies is not null + ? LookupResultUtils.FrequenciesToText(lookupResult.Frequencies, true, lookupResult.Frequencies.Count is 1) + : null; + case JLField.RawFrequencies: + if (lookupResult.Frequencies is not null) + { + List validFrequencies = lookupResult.Frequencies + .Where(static f => f.Freq is > 0 and < int.MaxValue).ToList(); + return string.Join(", ", validFrequencies.Select(static f => f.Freq).ToList()); + } + return null; + case JLField.PreferredFrequency: + if (lookupResult.Frequencies is not null) + { + int firstFrequency = lookupResult.Frequencies[0].Freq; + if (firstFrequency is > 0 and < int.MaxValue) + { + return firstFrequency.ToString(CultureInfo.InvariantCulture); + } + } + return null; + case JLField.FrequencyHarmonicMean: + if (lookupResult.Frequencies is not null) + { + List validFrequencies = lookupResult.Frequencies + .Where(static f => f.Freq is > 0 and < int.MaxValue).ToList(); + return CalculateHarmonicMean(validFrequencies).ToString(CultureInfo.InvariantCulture); + } + return null; + case JLField.PitchAccents: + { + if (DictUtils.SingleDictTypeDicts.TryGetValue(DictType.PitchAccentYomichan, out Dict? pitchDict) && pitchDict.Active) + { + List>? pitchAccents = GetPitchAccents(lookupResult.PitchAccentDict ?? pitchDict.Contents, lookupResult); + if (pitchAccents is not null) + { + StringBuilder expressionsWithPitchAccentBuilder = new(); + _ = expressionsWithPitchAccentBuilder.Append(CultureInfo.InvariantCulture, $"{PitchAccentStyle}\n\n"); + + int pitchAccentCount = pitchAccents.Count; + for (int i = 0; i < pitchAccentCount; i++) + { + KeyValuePair pitchAccent = pitchAccents[i]; + _ = expressionsWithPitchAccentBuilder.Append(GetExpressionWithPitchAccent(pitchAccent.Key, pitchAccent.Value)); + + if (i + 1 != pitchAccentCount) + { + _ = expressionsWithPitchAccentBuilder.Append('、'); + } + } + + return expressionsWithPitchAccentBuilder.ToString(); + } + } + return null; + } + case JLField.NumericPitchAccents: + { + if (DictUtils.SingleDictTypeDicts.TryGetValue(DictType.PitchAccentYomichan, out Dict? pitchDict) && pitchDict.Active) + { + List>? pitchAccents = GetPitchAccents(lookupResult.PitchAccentDict ?? pitchDict.Contents, lookupResult); + if (pitchAccents is not null) + { + StringBuilder numericPitchAccentBuilder = new(); + int pitchAccentCount = pitchAccents.Count; + for (int i = 0; i < pitchAccentCount; i++) + { + KeyValuePair pitchAccent = pitchAccents[i]; + _ = numericPitchAccentBuilder.Append(CultureInfo.InvariantCulture, $"{pitchAccent.Key}: {pitchAccent.Value}"); + + if (i + 1 != pitchAccentCount) + { + _ = numericPitchAccentBuilder.Append(", "); + } + } + + return numericPitchAccentBuilder.ToString(); + } + } + return null; + } + case JLField.PitchAccentForFirstReading: + { + if (DictUtils.SingleDictTypeDicts.TryGetValue(DictType.PitchAccentYomichan, out Dict? pitchDict) && pitchDict.Active) + { + List>? pitchAccents = GetPitchAccents(lookupResult.PitchAccentDict ?? pitchDict.Contents, lookupResult); + if (pitchAccents is not null) + { + KeyValuePair firstPitchAccentKeyValuePair = pitchAccents[0]; + if ((lookupResult.Readings is not null && firstPitchAccentKeyValuePair.Key == lookupResult.Readings[0]) + || (lookupResult.Readings is null && firstPitchAccentKeyValuePair.Key == lookupResult.PrimarySpelling)) + { + return string.Create(CultureInfo.InvariantCulture, $"{PitchAccentStyle}\n\n{GetExpressionWithPitchAccent(firstPitchAccentKeyValuePair.Key, firstPitchAccentKeyValuePair.Value)}"); + } + } + } + return null; + } + case JLField.NumericPitchAccentForFirstReading: + { + if (DictUtils.SingleDictTypeDicts.TryGetValue(DictType.PitchAccentYomichan, out Dict? pitchDict) && pitchDict.Active) + { + List>? pitchAccents = GetPitchAccents(lookupResult.PitchAccentDict ?? pitchDict.Contents, lookupResult); + if (pitchAccents is not null) + { + KeyValuePair firstPitchAccentKeyValuePair = pitchAccents[0]; + if ((lookupResult.Readings is not null && firstPitchAccentKeyValuePair.Key == lookupResult.Readings[0]) + || (lookupResult.Readings is null && firstPitchAccentKeyValuePair.Key == lookupResult.PrimarySpelling)) + { + return string.Create(CultureInfo.InvariantCulture, $"{firstPitchAccentKeyValuePair.Key}: {firstPitchAccentKeyValuePair.Value}"); + } + } + } + return null; + } + } + } + private static Dictionary GetMiningParameters(LookupResult lookupResult, string currentText, string? formattedDefinitions, string? selectedDefinitions, int currentCharPosition, bool useHtmlTags) { Dictionary miningParams = new() @@ -443,6 +685,82 @@ public static async Task MineToFile(LookupResult lookupResult, string currentTex Utils.Logger.Information("Mined {PrimarySpelling}", lookupResult.PrimarySpelling); } + public static async ValueTask CheckDuplicates(LookupResult[] lookupResults, string currentText, int currentCharPosition) + { + + Dictionary? ankiConfigDict = await AnkiConfig.ReadAnkiConfig().ConfigureAwait(false); + if (ankiConfigDict is null) + { + return null; + } + + List notes = []; + List positions = []; + bool[] results = new bool[lookupResults.Length]; + + for (int i = 0; i < lookupResults.Length; i++) + { + LookupResult lookupResult = lookupResults[i]; + + AnkiConfig? ankiConfig; + if (DictUtils.s_wordDictTypes.Contains(lookupResult.Dict.Type)) + { + ankiConfig = ankiConfigDict.GetValueOrDefault(MineType.Word); + } + else if (DictUtils.s_kanjiDictTypes.Contains(lookupResult.Dict.Type)) + { + ankiConfig = ankiConfigDict.GetValueOrDefault(MineType.Kanji); + } + else if (DictUtils.s_nameDictTypes.Contains(lookupResult.Dict.Type)) + { + ankiConfig = ankiConfigDict.GetValueOrDefault(MineType.Name); + } + else + { + ankiConfig = ankiConfigDict.GetValueOrDefault(MineType.Other); + } + + if (ankiConfig is null) + { + continue; + } + + Dictionary userFields = ankiConfig.Fields; + (string firstFieldName, JLField firstField) = userFields.First(); + string? firstFieldValue = GetMiningParameter(firstField, lookupResult, currentText, null, null, currentCharPosition); + if (string.IsNullOrEmpty(firstFieldValue)) + { + continue; + } + Dictionary fields = new(1, StringComparer.Ordinal) + { + { firstFieldName, firstFieldValue } + }; + + Note note = new(ankiConfig.DeckName, ankiConfig.ModelName, fields, null, null, null, null, null); + notes.Add(note); + positions.Add(i); + } + + if (notes.Count is 0) + { + return null; + } + + List? canAddNote = await AnkiUtils.CanAddNotes(notes).ConfigureAwait(false); + if (canAddNote is null) + { + return null; + } + + for (int i = 0; i < canAddNote.Count; i++) + { + results[positions[i]] = !canAddNote[i]; + } + + return results; + } + public static async Task Mine(LookupResult lookupResult, string currentText, string? formattedDefinitions, string? selectedDefinitions, int currentCharPosition) { CoreConfigManager coreConfigManager = CoreConfigManager.Instance; diff --git a/JL.Windows/ConfigManager.cs b/JL.Windows/ConfigManager.cs index 83b658aa..74868122 100644 --- a/JL.Windows/ConfigManager.cs +++ b/JL.Windows/ConfigManager.cs @@ -836,6 +836,7 @@ public void LoadPreferenceWindow(PreferencesWindow preferenceWindow) preferenceWindow.WebSocketUriTextBox.Text = coreConfigManager.WebSocketUri.OriginalString; preferenceWindow.ForceSyncAnkiCheckBox.IsChecked = coreConfigManager.ForceSyncAnki; preferenceWindow.AllowDuplicateCardsCheckBox.IsChecked = coreConfigManager.AllowDuplicateCards; + preferenceWindow.CheckForDuplicateCardsCheckBox.IsChecked = coreConfigManager.CheckForDuplicateCards; preferenceWindow.LookupRateNumericUpDown.Value = coreConfigManager.LookupRate; preferenceWindow.KanjiModeCheckBox.IsChecked = coreConfigManager.KanjiMode; preferenceWindow.AutoAdjustFontSizesOnResolutionChangeCheckBox.IsChecked = AutoAdjustFontSizesOnResolutionChange; @@ -1205,6 +1206,9 @@ public async Task SavePreferences(PreferencesWindow preferenceWindow) ConfigDBManager.UpdateSetting(connection, nameof(CoreConfigManager.AllowDuplicateCards), preferenceWindow.AllowDuplicateCardsCheckBox.IsChecked.ToString()!); + ConfigDBManager.UpdateSetting(connection, nameof(CoreConfigManager.CheckForDuplicateCards), + preferenceWindow.CheckForDuplicateCardsCheckBox.IsChecked.ToString()!); + ConfigDBManager.UpdateSetting(connection, nameof(CoreConfigManager.LookupRate), preferenceWindow.LookupRateNumericUpDown.Value.ToString(CultureInfo.InvariantCulture)); diff --git a/JL.Windows/GUI/PopupWindow.xaml.cs b/JL.Windows/GUI/PopupWindow.xaml.cs index 7ea9fa15..7806fc71 100644 --- a/JL.Windows/GUI/PopupWindow.xaml.cs +++ b/JL.Windows/GUI/PopupWindow.xaml.cs @@ -477,6 +477,8 @@ private void UpdatePosition(double x, double y) public void DisplayResults(bool generateAllResults) { + ConfigManager configManager = ConfigManager.Instance; + CoreConfigManager coreConfigManager = CoreConfigManager.Instance; _dictsWithResults.Clear(); PopupListView.Items.Filter = PopupWindowUtils.NoAllDictFilter; @@ -497,6 +499,15 @@ public void DisplayResults(bool generateAllResults) ? LastLookupResults.Length : Math.Min(LastLookupResults.Length, ConfigManager.Instance.MaxNumResultsNotInMiningMode); + bool checkForDuplicateCards = MiningMode + && coreConfigManager.CheckForDuplicateCards + && !configManager.MineToFileInsteadOfAnki + && coreConfigManager.AnkiIntegration; + + TextBlock[]? duplicateIcons = checkForDuplicateCards + ? new TextBlock[LastLookupResults.Length] + : null; + StackPanel[] popupItemSource = new StackPanel[resultCount]; for (int i = 0; i < resultCount; i++) @@ -508,10 +519,14 @@ public void DisplayResults(bool generateAllResults) _dictsWithResults.Add(lookupResult.Dict); } - popupItemSource[i] = PrepareResultStackPanel(lookupResult, i, resultCount, pitchDict, pitchDictIsActive, showPOrthographyInfo, showROrthographyInfo, showAOrthographyInfo, pOrthographyInfoFontSize); + popupItemSource[i] = PrepareResultStackPanel(lookupResult, i, resultCount, pitchDict, pitchDictIsActive, showPOrthographyInfo, showROrthographyInfo, showAOrthographyInfo, pOrthographyInfoFontSize, duplicateIcons); } PopupListView.ItemsSource = popupItemSource; + if (duplicateIcons is not null) + { + _ = CheckResultForDuplicates(duplicateIcons); + } GenerateDictTypeButtons(); UpdateLayout(); } @@ -536,7 +551,7 @@ private void AddEventHandlersToDefinitionsTextBox(TextBox textBox) textBox.PreviewMouseLeftButtonDown += DefinitionsTextBox_PreviewMouseLeftButtonDown; } - private StackPanel PrepareResultStackPanel(LookupResult result, int index, int resultCount, Dict? pitchDict, bool pitchDictIsActive, bool showPOrthographyInfo, bool showROrthographyInfo, bool showAOrthographyInfo, double pOrthographyInfoFontSize) + private StackPanel PrepareResultStackPanel(LookupResult result, int index, int resultCount, Dict? pitchDict, bool pitchDictIsActive, bool showPOrthographyInfo, bool showROrthographyInfo, bool showAOrthographyInfo, double pOrthographyInfoFontSize, TextBlock[]? duplicateIcons) { // top WrapPanel top = new() @@ -797,6 +812,29 @@ private StackPanel PrepareResultStackPanel(LookupResult result, int index, int r _ = top.Children.Add(dictTypeTextBlock); } + // Keep this at the bottom + if (duplicateIcons is not null) + { + TextBlock duplicate = new() + { + Text = "⚠", + Name = nameof(duplicate), + FontSize = configManager.DictTypeFontSize, + Foreground = configManager.DefinitionsColor, + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(7, 0, 0, 0), + HorizontalAlignment = HorizontalAlignment.Left, + Background = Brushes.Transparent, + Cursor = Cursors.Arrow, + Padding = new Thickness(0), + ToolTip = $"{result.PrimarySpelling} is already in the Anki deck.", + Visibility = Visibility.Hidden + }; + + _ = top.Children.Add(duplicate); + duplicateIcons[index] = duplicate; + } + // bottom StackPanel bottom = new(); @@ -1071,6 +1109,22 @@ private StackPanel PrepareResultStackPanel(LookupResult result, int index, int r return stackPanel; } + private async Task CheckResultForDuplicates(TextBlock[] duplicateIcons) + { + bool[]? duplicateCard = await MiningUtils.CheckDuplicates(LastLookupResults, _currentText, _currentCharPosition).ConfigureAwait(false); + + if (duplicateCard is not null) + { + for (int i = 0; i < duplicateCard.Length; i++) + { + if (duplicateCard[i]) + { + await MainWindow.Instance.Dispatcher.InvokeAsync(() => { duplicateIcons[i].Visibility = Visibility.Visible; }); + } + } + } + } + private int GetFirstVisibleListViewItemIndex() { StackPanel? firstVisibleStackPanel = PopupListView.Items.Cast() diff --git a/JL.Windows/GUI/PreferencesWindow.xaml b/JL.Windows/GUI/PreferencesWindow.xaml index 4a8cd61c..afb9dc82 100644 --- a/JL.Windows/GUI/PreferencesWindow.xaml +++ b/JL.Windows/GUI/PreferencesWindow.xaml @@ -1032,6 +1032,13 @@ + + + + +