Non-English users saw English text in a dozen places and blank labels in sixteen more. The English came from sites that produce their copy away from the widget that renders it, which is what the structural hardcoded-string check cannot see: picture-in-picture refused with a raw literal instead of the pipErrors.notSupported key that already existed; the two Jellyfin/Emby auth throws missing display: rendered their developer message on the add-server form; ServerParsingException.toString() fed its English into the localized "Failed to load servers" wrapper; Watch Together interpolated the whole PeerError, so a failed create read "Failed to create session: PeerError(PeerErrorType.timeout): Timed out creating session" and join printed its prefix twice; the hub and playlist continuation footers rendered exception.toString(); shader rows showed an English title over an already translated subtitle; the player queue fell back to the raw Dart enum name; a Plex home user with no title showed "Unknown"; a failed player start showed "Exception: Failed to initialize player"; and the tvOS top-shelf header was hardcoded in an extension that has no Flutter engine. The blanks came from three recent features that added English keys without translations. clean_translations.py filled all 21 siblings with empty strings, so the Android TV resolution switch, every Jellyfin/Emby recording-rule field, the demuxer row, the Companion Remote address caption and the Seerr blocklist pill rendered nothing at all. Two fixes are structural rather than key swaps. ContinuationStatusSliver now takes an errorContext and calls a new non-logging localizedLoadErrorText, so no future throw can leak through it. lib/mpv stays free of user-facing copy: it raises a PlayerInitializationException sentinel and a PlayerError.playerInitFailed cause tag that the player screen resolves to localized text. The tvOS section title travels in the shelf payload, additively, so an older cache still renders.
104 lines
3.8 KiB
Dart
104 lines
3.8 KiB
Dart
import '../i18n/strings.g.dart';
|
|
|
|
/// Backend-neutral classification of a media item.
|
|
///
|
|
/// Mirrors the categories in [PlexMediaType] but is the canonical type used by
|
|
/// neutral domain models. Each backend's adapter is responsible for mapping
|
|
/// its own type strings (Plex `type` field, Jellyfin `BaseItemKind`) into one
|
|
/// of these values.
|
|
enum MediaKind {
|
|
movie,
|
|
show,
|
|
season,
|
|
episode,
|
|
artist,
|
|
album,
|
|
track,
|
|
collection,
|
|
playlist,
|
|
clip,
|
|
photo,
|
|
|
|
/// Filesystem-style directory row in folder browsing (Plex `/folder`
|
|
/// listings, Jellyfin `Folder`/`CollectionFolder` items). Not playable
|
|
/// media itself — children are fetched via
|
|
/// `MediaServerClient.fetchFolderChildren`.
|
|
folder,
|
|
unknown;
|
|
|
|
bool get isVideo => this == movie || this == episode || this == clip;
|
|
|
|
bool get isMusic => this == artist || this == album || this == track;
|
|
|
|
bool get isPlayable => isVideo || this == track;
|
|
|
|
/// Whether this kind maps to one or more files on disk, i.e. the server can
|
|
/// answer `MediaServerClient.getFileInfo` for it.
|
|
///
|
|
/// Deliberately not folded into [isPlayable] even though the member sets
|
|
/// coincide today: container kinds (show, season, artist, album, collection,
|
|
/// playlist, folder) aggregate leaves and carry no `Media`/`MediaSources` of
|
|
/// their own, while a non-playable kind can still be file-backed.
|
|
bool get hasFileInfo => this == movie || this == episode || this == track || this == clip;
|
|
|
|
/// Whether this container kind derives watched state from aggregate leaves.
|
|
bool get usesLeafWatchCounts => switch (this) {
|
|
show || season || artist || album || collection || playlist || folder => true,
|
|
movie || episode || track || clip || photo || unknown => false,
|
|
};
|
|
|
|
/// Lowercase string id used when persisting or comparing legacy code paths
|
|
/// that still hold raw type strings.
|
|
String get id => switch (this) {
|
|
MediaKind.movie => 'movie',
|
|
MediaKind.show => 'show',
|
|
MediaKind.season => 'season',
|
|
MediaKind.episode => 'episode',
|
|
MediaKind.artist => 'artist',
|
|
MediaKind.album => 'album',
|
|
MediaKind.track => 'track',
|
|
MediaKind.collection => 'collection',
|
|
MediaKind.playlist => 'playlist',
|
|
MediaKind.clip => 'clip',
|
|
MediaKind.photo => 'photo',
|
|
MediaKind.folder => 'folder',
|
|
MediaKind.unknown => 'unknown',
|
|
};
|
|
|
|
/// Localized display noun for this kind. Unlike [id], which is the persisted/wire token, this is user-facing text.
|
|
String get label => switch (this) {
|
|
MediaKind.movie => t.common.mediaKind.movie,
|
|
MediaKind.show => t.common.mediaKind.show,
|
|
MediaKind.season => t.common.mediaKind.season,
|
|
MediaKind.episode => t.common.mediaKind.episode,
|
|
MediaKind.artist => t.common.mediaKind.artist,
|
|
MediaKind.album => t.common.mediaKind.album,
|
|
MediaKind.track => t.common.mediaKind.track,
|
|
MediaKind.collection => t.common.mediaKind.collection,
|
|
MediaKind.playlist => t.common.mediaKind.playlist,
|
|
MediaKind.clip => t.common.mediaKind.clip,
|
|
MediaKind.photo => t.common.mediaKind.photo,
|
|
MediaKind.folder => t.common.mediaKind.folder,
|
|
MediaKind.unknown => t.common.unknown,
|
|
};
|
|
|
|
static MediaKind fromString(String? raw) {
|
|
if (raw == null) return MediaKind.unknown;
|
|
return switch (raw.toLowerCase()) {
|
|
'movie' => MediaKind.movie,
|
|
'show' || 'series' => MediaKind.show,
|
|
'season' => MediaKind.season,
|
|
'episode' => MediaKind.episode,
|
|
'artist' || 'musicartist' => MediaKind.artist,
|
|
'album' || 'musicalbum' => MediaKind.album,
|
|
'track' || 'audio' => MediaKind.track,
|
|
'collection' || 'boxset' => MediaKind.collection,
|
|
'playlist' => MediaKind.playlist,
|
|
'clip' || 'trailer' || 'video' || 'musicvideo' => MediaKind.clip,
|
|
'photo' => MediaKind.photo,
|
|
'folder' || 'collectionfolder' => MediaKind.folder,
|
|
_ => MediaKind.unknown,
|
|
};
|
|
}
|
|
}
|