Refactor PLib video library manager to use ReactiveUI, replacing CommunityToolkit.Mvvm. Update dependencies, enhance thumbnail caching logic, and improve UI responsiveness with reactive commands. Remove obsolete PLib.slnx file and update README.md to reflect changes.
This commit is contained in:
@@ -1,66 +1,79 @@
|
||||
# PLib
|
||||
|
||||
Менеджер видеотеки на Avalonia: сканирует папки, вытаскивает превью через ffmpeg и
|
||||
показывает всё сеткой карточек.
|
||||
|
||||
## Что уже работает
|
||||
|
||||
- Сканирование указанных папок, инкрементальное — файл, который не изменился, не переиндексируется.
|
||||
- Метаданные (длительность, разрешение, кодек) через ffprobe.
|
||||
- Постеры кадром из видео через ffmpeg, с кэшем на диске.
|
||||
- Виртуализированная сетка карточек, ленивая загрузка превью, поиск и сортировка.
|
||||
- Светлая и тёмная темы.
|
||||
- Клик или Enter по карточке — открыть в системном плеере, правая кнопка — контекстное меню.
|
||||
|
||||
## Требования
|
||||
|
||||
- .NET 10 SDK
|
||||
- `ffmpeg` и `ffprobe` в `PATH`
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
dotnet run --project src/PLib.Desktop
|
||||
```
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
## Архитектура
|
||||
|
||||
Четыре слоя, зависимости направлены только внутрь:
|
||||
|
||||
| Проект | Отвечает за | Знает о |
|
||||
| --- | --- | --- |
|
||||
| `PLib.Domain` | Сущность `VideoItem` и её инварианты | ни о чём |
|
||||
| `PLib.Application` | Сценарии (`LibraryService`) и абстракции портов | Domain |
|
||||
| `PLib.Infrastructure` | EF Core + SQLite, ffmpeg, файловая система | Application |
|
||||
| `PLib.Desktop` | Avalonia, ViewModel'и, composition root | Infrastructure |
|
||||
|
||||
Ключевые решения:
|
||||
|
||||
- **Сканирование — поток событий.** `ILibraryService.ScanAsync` возвращает
|
||||
`IAsyncEnumerable<LibraryScanEvent>`: карточки появляются по мере находок, а не после
|
||||
завершения всего прохода. Тяжёлая часть (ffprobe + ffmpeg) идёт параллельно через
|
||||
`Parallel.ForEachAsync`, результаты собираются в `Channel` и применяются к сущностям
|
||||
по одному — трекер изменений EF не потокобезопасен.
|
||||
- **Вся работа вне UI-потока.** ViewModel оборачивает конвейер в `Task.Run` и возвращает
|
||||
каждое событие в UI явно через `Dispatcher.UIThread`.
|
||||
- **Превью живут только пока видны.** `AsyncImage` запрашивает битмап при попадании в
|
||||
визуальное дерево и отпускает при выходе; `ThumbnailCache` — LRU на 256 записей с
|
||||
декодированием в нужную ширину. Память зависит от размера окна, а не от размера библиотеки.
|
||||
- **Scope на операцию.** `DbContext` живёт ровно одну операцию — ViewModel берёт
|
||||
`IServiceScopeFactory` и создаёт scope на каждый вызов.
|
||||
|
||||
## Данные
|
||||
|
||||
Всё пользовательское лежит в `%LOCALAPPDATA%\PLib`:
|
||||
|
||||
- `library.db` — SQLite с метаданными;
|
||||
- `thumbnails/` — кэш постеров (ключ = путь + размер + время изменения файла);
|
||||
- `settings.json` — список папок, перечитывается на лету;
|
||||
- `logs/` — Serilog, ротация по дням.
|
||||
|
||||
Схема создаётся через `EnsureCreated`. Когда форма таблицы устоится — заменить на
|
||||
миграции EF Core (`DatabaseInitializer` — единственное место, которое надо будет тронуть).
|
||||
# PLib
|
||||
|
||||
Менеджер видеотеки на Avalonia: сканирует папки, вытаскивает превью через ffmpeg и
|
||||
показывает всё сеткой карточек.
|
||||
|
||||
## Что уже работает
|
||||
|
||||
- Сканирование указанных папок, инкрементальное — файл, который не изменился, не переиндексируется.
|
||||
- Метаданные (длительность, разрешение, кодек) через ffprobe.
|
||||
- Постеры кадром из видео через ffmpeg, с кэшем на диске.
|
||||
- Виртуализированная сетка карточек, ленивая загрузка превью, поиск и сортировка.
|
||||
- Светлая и тёмная темы.
|
||||
- Клик или Enter по карточке — открыть в системном плеере, правая кнопка — контекстное меню.
|
||||
|
||||
## Требования
|
||||
|
||||
- .NET 10 SDK
|
||||
- `ffmpeg` и `ffprobe` в `PATH`
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
dotnet run --project src/PLib.Desktop
|
||||
```
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
## Архитектура
|
||||
|
||||
Четыре слоя, зависимости направлены только внутрь:
|
||||
|
||||
| Проект | Отвечает за | Знает о |
|
||||
| --- | --- | --- |
|
||||
| `PLib.Domain` | Сущность `VideoItem` и её инварианты | ни о чём |
|
||||
| `PLib.Application` | Сценарии (`LibraryService`) и абстракции портов | Domain |
|
||||
| `PLib.Infrastructure` | EF Core + SQLite, ffmpeg, файловая система | Application |
|
||||
| `PLib.Desktop` | Avalonia, ViewModel'и, composition root | Infrastructure |
|
||||
|
||||
Ключевые решения:
|
||||
|
||||
- **MVVM на ReactiveUI.** Свойства — `[Reactive]` из `ReactiveUI.SourceGenerators`, команды —
|
||||
`ReactiveCommand`, производные значения (`IsScanning`, `IsEmpty`) — `ToProperty`. Отмена
|
||||
сканирования сделана штатным способом: скан живёт как observable, а `CancelScanCommand`
|
||||
просто отписывает его через `TakeUntil`, что отменяет `CancellationToken`.
|
||||
- **Сетка — проекция DynamicData, а не пересборка.** `SourceCache` → `AutoRefresh` → `Filter`
|
||||
→ `SortAndBind` отдаёт диффы: добавился один файл — одна вставка в нужную позицию. Скролл,
|
||||
контейнеры `ItemsRepeater` и уже загруженные превью остаются на месте. Поиск дебаунсится
|
||||
на 200 мс, изменения карточек во время скана коалесцируются в 250 мс.
|
||||
- **Сканирование — поток событий.** `ILibraryService.ScanAsync` возвращает
|
||||
`IAsyncEnumerable<LibraryScanEvent>`: карточки появляются по мере находок, а не после
|
||||
завершения всего прохода. Тяжёлая часть (ffprobe + ffmpeg) идёт параллельно через
|
||||
`Parallel.ForEachAsync`, результаты собираются в `Channel` и применяются к сущностям
|
||||
по одному — трекер изменений EF не потокобезопасен.
|
||||
- **Вся работа вне UI-потока.** ViewModel оборачивает конвейер в `Task.Run` и возвращает
|
||||
каждое событие в UI явно через `Dispatcher.UIThread`.
|
||||
- **Превью живут только пока видны.** `AsyncImage` запрашивает битмап при попадании в
|
||||
визуальное дерево и отпускает при выходе; `ThumbnailCache` — LRU на 256 записей с
|
||||
декодированием в нужную ширину. Память зависит от размера окна, а не от размера библиотеки.
|
||||
- **Scope на операцию.** `DbContext` живёт ровно одну операцию — ViewModel берёт
|
||||
`IServiceScopeFactory` и создаёт scope на каждый вызов.
|
||||
- **Кэш превью самовосстанавливается.** Диск — ключ `sha256(путь|размер|mtime)`, память — LRU
|
||||
на 256 декодированных битмапов. Сканирование проверяет, что запомненный кадр физически
|
||||
на месте (`IThumbnailGenerator.IsAvailable`), и перерисовывает удалённые; после полного
|
||||
прохода лишние файлы вычищаются (`PurgeUnusedAsync`). Незавершённые `.tmp` удаляются
|
||||
только если им больше часа — иначе можно снести рендер второго запущенного экземпляра.
|
||||
|
||||
## Данные
|
||||
|
||||
Всё пользовательское лежит в `%LOCALAPPDATA%\PLib`:
|
||||
|
||||
- `library.db` — SQLite с метаданными;
|
||||
- `thumbnails/` — кэш постеров (ключ = путь + размер + время изменения файла);
|
||||
- `settings.json` — список папок, перечитывается на лету;
|
||||
- `logs/` — Serilog, ротация по дням.
|
||||
|
||||
Схема создаётся через `EnsureCreated`. Когда форма таблицы устоится — заменить на
|
||||
миграции EF Core (`DatabaseInitializer` — единственное место, которое надо будет тронуть).
|
||||
|
||||
Reference in New Issue
Block a user