Skip to content

Commit 3cbca0d

Browse files
authored
Merge pull request #6 from Scream034/Fix-ui-1
Исправление: Перемещения треков; Не использование ID MAP везде. Рефакторинг (DRY).
2 parents 7ba76e4 + 8eff8c5 commit 3cbca0d

12 files changed

Lines changed: 882 additions & 426 deletions

File tree

Core/Helpers/TrackFilters.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using LMP.Core.Models;
2+
3+
namespace LMP.Core.Helpers;
4+
5+
/// <summary>
6+
/// Общие методы фильтрации треков для устранения дублирования.
7+
/// </summary>
8+
public static class TrackFilters
9+
{
10+
/// <summary>
11+
/// Фильтрует трек по названию и автору.
12+
/// </summary>
13+
public static bool MatchesTitleOrAuthor(TrackInfo track, string query)
14+
{
15+
if (string.IsNullOrWhiteSpace(query)) return true;
16+
17+
return track.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
18+
track.Author.Contains(query, StringComparison.OrdinalIgnoreCase);
19+
}
20+
21+
/// <summary>
22+
/// Фильтрует трек с дополнительными полями (для расширенного поиска).
23+
/// </summary>
24+
public static bool MatchesExtended(TrackInfo track, string query)
25+
{
26+
if (string.IsNullOrWhiteSpace(query)) return true;
27+
28+
return track.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
29+
track.Author.Contains(query, StringComparison.OrdinalIgnoreCase) ||
30+
(track.ChannelId?.Contains(query, StringComparison.OrdinalIgnoreCase) ?? false);
31+
}
32+
}

Core/Models/RangeMap.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,19 @@ public bool IsFullyDownloaded(long totalLength)
8686
{
8787
if (_ranges.Count == 0)
8888
{
89-
Log.Debug($"[RangeMap] IsFullyDownloaded: NO ranges, total={totalLength}");
89+
// Log.Debug($"IsFullyDownloaded: NO ranges, total={totalLength}");
9090
return false;
9191
}
9292

9393
if (_ranges.Count > 1)
9494
{
95-
Log.Debug($"[RangeMap] IsFullyDownloaded: {_ranges.Count} ranges (not merged?), total={totalLength}");
95+
// Log.Debug($"IsFullyDownloaded: {_ranges.Count} ranges (not merged?), total={totalLength}");
9696
return false;
9797
}
9898

9999
var range = _ranges[0];
100100
bool result = range.Start == 0 && range.End >= totalLength;
101101

102-
if (!result)
103-
{
104-
Log.Debug($"[RangeMap] IsFullyDownloaded: range=[{range.Start}-{range.End}], total={totalLength}, result={result}");
105-
}
106-
107102
return result;
108103
}
109104
}

Core/Services/TrackViewModelFactory.cs

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,70 +6,96 @@ namespace LMP.Core.Services;
66

77
/// <summary>
88
/// Фабрика для создания TrackItemViewModel.
9-
/// Использует кэширование на основе WeakReference.
9+
/// Использует TrackRegistry для Identity Map и кэширует ViewModel-и.
1010
/// </summary>
11-
public class TrackViewModelFactory(
12-
AudioEngine audio,
13-
LibraryService library,
14-
DownloadService downloads,
15-
MusicLibraryManager manager)
11+
public class TrackViewModelFactory
1612
{
17-
private readonly AudioEngine _audio = audio;
18-
private readonly LibraryService _library = library;
19-
private readonly DownloadService _downloads = downloads;
20-
private readonly MusicLibraryManager _manager = manager;
13+
private readonly AudioEngine _audio;
14+
private readonly LibraryService _library;
15+
private readonly DownloadService _downloads;
16+
private readonly MusicLibraryManager _manager;
17+
private readonly TrackRegistry _registry;
18+
private readonly StreamCacheManager _cacheManager;
2119

2220
private readonly ConcurrentDictionary<string, WeakReference<TrackItemViewModel>> _cache = new();
2321

22+
public TrackViewModelFactory(
23+
AudioEngine audio,
24+
LibraryService library,
25+
DownloadService downloads,
26+
MusicLibraryManager manager,
27+
TrackRegistry registry,
28+
StreamCacheManager cacheManager)
29+
{
30+
_audio = audio;
31+
_library = library;
32+
_downloads = downloads;
33+
_manager = manager;
34+
_registry = registry;
35+
_cacheManager = cacheManager;
36+
}
37+
2438
/// <summary>
25-
/// Возвращает существующую или создает новую ViewModel для трека.
39+
/// Возвращает существующую или создаёт новую ViewModel для трека.
2640
/// </summary>
2741
public TrackItemViewModel GetOrCreate(TrackInfo track, Action<TrackInfo>? playAction = null)
2842
{
29-
// 1. Проверяем кэш
30-
if (_cache.TryGetValue(track.Id, out var weakRef) &&
31-
weakRef.TryGetTarget(out var existing) &&
43+
// Identity Map — получаем канонический экземпляр
44+
var canonical = _registry.RegisterOrUpdate(track);
45+
46+
// Проверяем кэш
47+
if (_cache.TryGetValue(canonical.Id, out var weakRef) &&
48+
weakRef.TryGetTarget(out var existing) &&
3249
!existing.IsDisposed)
3350
{
34-
// Обновляем контекст использования
3551
existing.UpdatePlayAction(playAction);
3652
existing.IsQueueContext = false;
3753

38-
// НЕ нужно синхронизировать IsLiked/IsDownloaded —
39-
// они автоматически берутся из Track через ObservableAsPropertyHelper
40-
41-
return existing;
54+
if (!ReferenceEquals(existing.Track, canonical))
55+
{
56+
Log.Warn($"[TrackFactory] VM track mismatch for {canonical.Id}, recreating");
57+
existing.Dispose();
58+
}
59+
else
60+
{
61+
return existing;
62+
}
4263
}
4364

44-
// 2. Создаем новую
65+
// Создаём новую VM
4566
var vm = new TrackItemViewModel(
46-
track,
67+
canonical,
4768
_audio,
4869
_library,
4970
_downloads,
5071
_manager,
72+
_cacheManager, // ← Передаём через DI
5173
playAction);
5274

53-
// 3. Сохраняем в кэш
54-
_cache[track.Id] = new WeakReference<TrackItemViewModel>(vm);
75+
_cache[canonical.Id] = new WeakReference<TrackItemViewModel>(vm);
5576

5677
return vm;
5778
}
5879

59-
/// <summary>
60-
/// Создает VM для использования в очереди воспроизведения.
61-
/// </summary>
6280
public TrackItemViewModel CreateForQueue(TrackInfo track, Action<TrackInfo>? playAction = null)
6381
{
6482
var vm = GetOrCreate(track, playAction);
6583
vm.IsQueueContext = true;
6684
return vm;
6785
}
6886

69-
/// <summary>
70-
/// Удаляет "мертвые" ссылки из кэша.
71-
/// </summary>
72-
public void CleanupCache()
87+
public TrackItemViewModel? TryGet(string trackId)
88+
{
89+
if (_cache.TryGetValue(trackId, out var weakRef) &&
90+
weakRef.TryGetTarget(out var vm) &&
91+
!vm.IsDisposed)
92+
{
93+
return vm;
94+
}
95+
return null;
96+
}
97+
98+
public int CleanupCache()
7399
{
74100
var deadKeys = _cache
75101
.Where(kvp => !kvp.Value.TryGetTarget(out var target) || target.IsDisposed)
@@ -81,11 +107,10 @@ public void CleanupCache()
81107

82108
if (deadKeys.Count > 0)
83109
Log.Info($"[TrackFactory] Cleaned {deadKeys.Count} dead items.");
110+
111+
return deadKeys.Count;
84112
}
85113

86-
/// <summary>
87-
/// Полностью очищает кэш.
88-
/// </summary>
89114
public void Clear()
90115
{
91116
foreach (var kvp in _cache)

Core/ViewModels/PaginatedViewModel.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.ObjectModel;
1+
// Core/ViewModels/PaginatedViewModel.cs
2+
using System.Collections.ObjectModel;
23
using System.Reactive;
34
using System.Reactive.Disposables;
45
using System.Reactive.Linq;
@@ -23,7 +24,7 @@ public abstract class PaginatedViewModel<TSource, TViewModel> : ViewModelBase, I
2324
private readonly ReadOnlyObservableCollection<TViewModel> _items;
2425
private readonly CompositeDisposable _cleanUp = [];
2526

26-
private int _consecutiveEmptyLoads = 0;
27+
private int _consecutiveEmptyLoads;
2728
private const int MaxConsecutiveEmptyLoads = 5;
2829

2930
private CancellationTokenSource? _loadCts;
@@ -139,15 +140,23 @@ private Func<TSource, bool> BuildFilterPredicate(string query)
139140

140141
#region Public Methods
141142

143+
/// <summary>
144+
/// Перемещает элемент. Использует RemoveAt+Insert вместо Move
145+
/// для корректной работы с DynamicData Filter.
146+
/// </summary>
142147
protected void MoveSourceItem(int oldIndex, int newIndex)
143148
{
144149
_sourceList.Edit(list =>
145150
{
146-
if (oldIndex >= 0 && oldIndex < list.Count &&
147-
newIndex >= 0 && newIndex < list.Count)
148-
{
149-
list.Move(oldIndex, newIndex);
150-
}
151+
if (oldIndex < 0 || oldIndex >= list.Count ||
152+
newIndex < 0 || newIndex >= list.Count ||
153+
oldIndex == newIndex)
154+
return;
155+
156+
// RemoveAt + Insert вместо Move — Filter корректно обрабатывает эти события
157+
var item = list[oldIndex];
158+
list.RemoveAt(oldIndex);
159+
list.Insert(newIndex, item);
151160
});
152161
}
153162

0 commit comments

Comments
 (0)