feat(file-info): detail every version, file and stream the server reports

The sheet collapsed an item to `Media.first` / `MediaSources.first` and
rendered a fixed set of rows, so split files, extra versions, per-track
properties, HDR classification and Dolby Vision were all invisible.

Model the payload the way both servers shape it — versions own parts,
parts own streams — and project every property either backend populates
onto `MediaStreamDetails`. The field set comes from sweeping both test
servers in full through the clients' own request shapes (Plex 6842
Media / 6842 Part / 39864 Stream entries, Jellyfin 6349 sources / 37763
streams / 61224 attachments), so file presence, Dolby Vision layers,
dynamic range, sample rate, spatial audio, sidecar provenance, embedded
attachments, rotation and the lyric stream type all survive. A coverage
test fails when a server key is neither carried, folded into a sibling,
nor excluded with a reason.

File Info also stopped trusting the shared `/library/metadata/{id}`
cache row: `getPlaybackExtras` writes it without `includeStreams` /
`checkFiles`, so the sheet could render with no stream table at all.
Detect that shape and refetch once under the request context captured
before the cache read, so the outgoing token and the cache namespace
stay on one profile.

Rework the layout to match: a summary chip row, then flat tonal cards
with a two-column field grid that collapses to one column on narrow
viewports, per-stream cards with flag chips, and a copyable monospace
path row. The card fill is a tonal step off the text colour rather than
the `bg` token, which is one shade from the sheet surface on OLED.
This commit is contained in:
edde746
2026-07-29 05:19:27 +02:00
parent 69ceea9466
commit 0ef71e6489
58 changed files with 6429 additions and 515 deletions
+4 -6
View File
@@ -507,8 +507,8 @@ class _Translations$fileInfo$ru extends Translations$fileInfo$en {
@override String get title => 'Информация о файле';
@override String get video => 'Видео';
@override String get audio => 'Аудио';
@override String get subtitles => 'Субтитры';
@override String get file => 'Файл';
@override String get advanced => 'Дополнительно';
@override String get codec => 'Кодек';
@override String get resolution => 'Разрешение';
@override String get bitrate => 'Битрейт';
@@ -521,7 +521,6 @@ class _Translations$fileInfo$ru extends Translations$fileInfo$en {
@override String get colorPrimaries => 'Основные цвета';
@override String get chromaSubsampling => 'Субдискретизация цветности';
@override String get channels => 'Каналы';
@override String get subtitles => 'Субтитры';
@override String get overallBitrate => 'Общий битрейт';
@override String get path => 'Путь';
@override String get size => 'Размер';
@@ -2566,8 +2565,8 @@ extension on TranslationsRu {
'fileInfo.title' => 'Информация о файле',
'fileInfo.video' => 'Видео',
'fileInfo.audio' => 'Аудио',
'fileInfo.subtitles' => 'Субтитры',
'fileInfo.file' => 'Файл',
'fileInfo.advanced' => 'Дополнительно',
'fileInfo.codec' => 'Кодек',
'fileInfo.resolution' => 'Разрешение',
'fileInfo.bitrate' => 'Битрейт',
@@ -2580,7 +2579,6 @@ extension on TranslationsRu {
'fileInfo.colorPrimaries' => 'Основные цвета',
'fileInfo.chromaSubsampling' => 'Субдискретизация цветности',
'fileInfo.channels' => 'Каналы',
'fileInfo.subtitles' => 'Субтитры',
'fileInfo.overallBitrate' => 'Общий битрейт',
'fileInfo.path' => 'Путь',
'fileInfo.size' => 'Размер',
@@ -2716,9 +2714,9 @@ extension on TranslationsRu {
'videoControls.language' => 'Язык',
'videoControls.noSubtitlesFound' => 'Субтитры не найдены',
'videoControls.subtitleDownloaded' => 'Субтитры загружены',
'videoControls.subtitleDownloadedNotApplied' => 'Субтитры загружены, но их не удалось выбрать',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Субтитры загружены, но их не удалось выбрать',
'videoControls.subtitleDownloadFailed' => 'Не удалось загрузить субтитры',
'videoControls.searchLanguages' => 'Поиск языков...',
'messages.markedAsWatched' => 'Отмечено как просмотренное',
@@ -3230,9 +3228,9 @@ extension on TranslationsRu {
'watchTogether.failedToJoin' => 'Не удалось присоединиться к сессии',
'watchTogether.sessionCodeCopied' => 'Код сессии скопирован в буфер обмена',
'watchTogether.relayUnreachable' => 'Сервер ретрансляции недоступен. Блокировка интернет-провайдером может помешать совместному просмотру.',
'watchTogether.reconnectingToHost' => 'Повторное подключение к организатору...',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Повторное подключение к организатору...',
'watchTogether.currentPlayback' => 'Текущее воспроизведение',
'watchTogether.joinCurrentPlayback' => 'Присоединиться к текущему воспроизведению',
'watchTogether.joinCurrentPlaybackDescription' => 'Вернуться к материалу, который сейчас смотрит организатор',