@@ -10,6 +10,7 @@ import 'media_backend.dart';
|
||||
import 'media_kind.dart';
|
||||
import 'media_role.dart';
|
||||
import 'media_version.dart';
|
||||
import 'season_title.dart';
|
||||
|
||||
part 'media_item.freezed.dart';
|
||||
part 'media_item.g.dart';
|
||||
@@ -401,8 +402,14 @@ sealed class MediaItem with _$MediaItem {
|
||||
|
||||
/// Subtitle line shown below [displayTitle] for episodes/seasons.
|
||||
String? get displaySubtitle {
|
||||
if (kind == MediaKind.episode || kind == MediaKind.season) {
|
||||
if (grandparentTitle != null || (kind == MediaKind.season && parentTitle != null)) {
|
||||
if (kind == MediaKind.season) {
|
||||
if (grandparentTitle != null || parentTitle != null) {
|
||||
// Re-localize a server's generic English "Season N" (see #1271).
|
||||
final label = localizedSeasonLabel(title: title, index: index);
|
||||
return label.isNotEmpty ? label : title;
|
||||
}
|
||||
} else if (kind == MediaKind.episode) {
|
||||
if (grandparentTitle != null) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'media_item.dart';
|
||||
import 'media_kind.dart';
|
||||
import 'season_title.dart';
|
||||
|
||||
/// Convenience type-check getters and spoiler helpers on [MediaItem]. These
|
||||
/// give consumers a Plex-style fluent API (e.g. `item.isShow`) while keeping
|
||||
@@ -27,4 +28,9 @@ extension MediaItemTypes on MediaItem {
|
||||
|
||||
/// Non-spoiler art path for episodes (show/season background).
|
||||
String? get spoilerSafeArt => grandparentArtPath ?? artPath;
|
||||
|
||||
/// Localized season label for this item. Re-localizes a server's generic
|
||||
/// English "Season N" to the app locale (see [localizedSeasonLabel]); falls
|
||||
/// back to [displayTitle] when there is no usable title or index.
|
||||
String get localizedSeasonTitle => localizedSeasonLabel(title: title, index: index, fallback: displayTitle);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// Matches a media server's generic English season title, e.g. "Season 1".
|
||||
///
|
||||
/// Plex and Jellyfin return this verbatim when they have no localized name for a
|
||||
/// season (or when the request language resolves to English), which leaks
|
||||
/// untranslated "Season N" labels into otherwise-localized UI (see #1271).
|
||||
final RegExp _genericSeasonTitle = RegExp(r'^season\s+(\d+)$', caseSensitive: false);
|
||||
|
||||
/// Localized season label.
|
||||
///
|
||||
/// Replaces a generic English "Season N" (or an empty title) with the app-locale
|
||||
/// `t.common.seasonNumber`, while preserving custom names ("Specials", …) and
|
||||
/// titles the server already localized ("Saison 1").
|
||||
///
|
||||
/// Prefers [index] for the number; falls back to the digits parsed from a
|
||||
/// generic title, then to [fallback] when there is nothing usable to show.
|
||||
String localizedSeasonLabel({String? title, int? index, String? fallback}) {
|
||||
final raw = title?.trim() ?? '';
|
||||
final match = _genericSeasonTitle.firstMatch(raw);
|
||||
final number = index ?? (match != null ? int.tryParse(match.group(1)!) : null);
|
||||
if ((match != null || raw.isEmpty) && number != null) {
|
||||
return t.common.seasonNumber(number: number);
|
||||
}
|
||||
return raw.isNotEmpty ? raw : (fallback ?? '');
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import '../media/media_hub.dart';
|
||||
import '../utils/provider_extensions.dart';
|
||||
import '../utils/plex_season_display.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../media/season_title.dart';
|
||||
import '../media/episode_collection.dart';
|
||||
import '../media/media_item_types.dart';
|
||||
import '../media/media_kind.dart';
|
||||
@@ -1669,7 +1670,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
id: seasonId,
|
||||
backend: _metadata.backend,
|
||||
kind: MediaKind.season,
|
||||
title: firstEp.parentTitle ?? t.common.seasonNumber(number: entry.key),
|
||||
title: localizedSeasonLabel(title: firstEp.parentTitle, index: entry.key),
|
||||
index: entry.key,
|
||||
leafCount: entry.value.length,
|
||||
thumbPath: firstEp.parentThumbPath,
|
||||
@@ -2335,7 +2336,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
onSecondaryTapDown: (details) => tapPosition = details.globalPosition,
|
||||
onSecondaryTap: () => _showSeasonTabContextMenu(index, position: tapPosition),
|
||||
child: FocusableTabChip(
|
||||
label: season.title!,
|
||||
label: season.localizedSeasonTitle,
|
||||
isSelected: index == _selectedSeasonIndex,
|
||||
topImage: topImage,
|
||||
focusNode: _seasonTabFocusNodes.length > index ? _seasonTabFocusNodes[index] : null,
|
||||
@@ -3739,7 +3740,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
||||
hubs.add(
|
||||
MediaHub(
|
||||
id: '$_tvDetailSeasonHubIdPrefix$i',
|
||||
title: season.title?.isNotEmpty == true ? season.title! : (season.displaySubtitle ?? season.displayTitle),
|
||||
title: season.localizedSeasonTitle,
|
||||
type: 'episode',
|
||||
items: episodes,
|
||||
size: total,
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
||||
import '../focus/input_mode_tracker.dart';
|
||||
import '../media/media_item.dart';
|
||||
import '../media/media_item_types.dart';
|
||||
import '../media/season_title.dart';
|
||||
import '../media/media_kind.dart';
|
||||
import '../media/media_playlist.dart';
|
||||
import '../mixins/context_menu_tap_mixin.dart';
|
||||
@@ -133,7 +134,7 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
final episodeInfo = item.parentIndex != null && item.index != null ? 'S${item.parentIndex} E${item.index}' : '';
|
||||
baseLabel = t.accessibility.mediaCardEpisode(title: item.displayTitle, episodeInfo: episodeInfo);
|
||||
case MediaKind.season:
|
||||
final seasonInfo = item.parentIndex != null ? 'Season ${item.parentIndex}' : '';
|
||||
final seasonInfo = localizedSeasonLabel(title: item.title, index: item.index);
|
||||
baseLabel = t.accessibility.mediaCardSeason(title: item.displayTitle, seasonInfo: seasonInfo);
|
||||
case MediaKind.movie:
|
||||
baseLabel = t.accessibility.mediaCardMovie(title: item.displayTitle);
|
||||
|
||||
Reference in New Issue
Block a user