Files
plezy/lib/utils/rating_utils.dart
T
edde746 53288116fe fix(explore): size catalog detail relations, ratings and facts to their content
Four sections of the catalog detail screen spent more room than their data
justified.

Franchise relations drew one hub shelf per label. Real payloads make that
absurd: MAL returns twelve relations for Attack on Titan across six labels,
and "Side story" and "Sequel" each hold exactly one title, so each spent a
header, a scroll row and one card. Flatten the labelled groups into one
"Related titles" section of compact rows — poster thumb, label, title and year
— that flow into columns on wide viewports. D-pad moves through the grid by
index and still hands off to the cast strip above and the recommendations
shelf below, which keeps its shelf because taste-based recommendations are
meant to be browsed.

Drop the MAL picture gallery. It was a horizontal strip of unfocusable poster
variants of the title you are already looking at, and it cost a page-height of
scroll; the `pictures` field comes back out of the detail request with it.

Draw attributed scores behind their own brand mark where the source has one,
the way the media detail screen already does: Rotten Tomatoes fresh/rotten and
upright/spilled, IMDb and TMDB, each on the scale that source publishes.
Sources with no mark (critic, audience, tracker scores) keep their written
label. Plex's own badge state is derived from the 60% tomatometer threshold it
encodes in `image.rating.ripe`.

Flow the definition rows — original title, studios, country, budget, box
office, crew — into two or three columns once the window is wide enough.
A 1440-wide window drew a 140-pixel label, a short value and 1,000 pixels of
nothing per fact.

Verified against live MAL and Plex Discover payloads on macOS: the Attack on
Titan page drops from 4,082 to 2,115 logical pixels, Dune: Part Two from 1,282
to 1,154.
2026-07-30 01:01:38 +02:00

78 lines
3.0 KiB
Dart

/// Resolves the brand badge shown beside a score: Plex's
/// ratingImage/audienceRatingImage URIs, and the attributed source keys the
/// Explore catalog carries instead of them.
library;
class RatingInfo {
final String assetPath;
final String formattedValue;
const RatingInfo(this.assetPath, this.formattedValue);
}
/// Parse a ratingImage URI (e.g. "rottentomatoes://image.rating.ripe")
/// together with the numeric rating value into a [RatingInfo].
///
/// Returns null if the URI is unrecognised.
RatingInfo? parseRatingImage(String? imageUri, double? value) {
if (imageUri == null || value == null) return null;
if (imageUri.startsWith('rottentomatoes://image.rating.')) {
final suffix = imageUri.substring('rottentomatoes://image.rating.'.length);
return switch (suffix) {
'ripe' => RatingInfo(_rtFreshAsset, _percent(value)),
'rotten' => RatingInfo(_rtRottenAsset, _percent(value)),
'upright' => RatingInfo(_rtUprightAsset, _percent(value)),
'spilled' => RatingInfo(_rtSpilledAsset, _percent(value)),
_ => null,
};
}
if (imageUri.startsWith('imdb://')) {
return RatingInfo(_imdbAsset, value.toStringAsFixed(1));
}
if (imageUri.startsWith('themoviedb://')) {
return RatingInfo(_tmdbAsset, _percent(value));
}
return null;
}
/// Whether the URI is a Rotten Tomatoes rating source.
bool isRottenTomatoes(String? imageUri) => imageUri != null && imageUri.startsWith('rottentomatoes://');
/// The same badge for a [CatalogRatingSource]-style source key.
///
/// Catalog providers attribute their scores by name (`imdb`, `tmdb`,
/// `rottenTomatoesCritic`, …) and publish no badge URI, so the icon is chosen
/// from the key. Rotten Tomatoes picks fresh/rotten and upright/spilled by the
/// 60% threshold the tomatometer itself uses — the same state Plex encodes in
/// `image.rating.ripe` / `.rotten`.
///
/// Returns null for keys with no brand badge (`critic`, `audience`, `simkl`,
/// `mal`, `anilist`, `trakt`); those stay labelled with their source name.
RatingInfo? catalogRatingInfo(String source, double value) => switch (source) {
'imdb' => RatingInfo(_imdbAsset, value.toStringAsFixed(1)),
'tmdb' => RatingInfo(_tmdbAsset, _percent(value)),
'rottenTomatoes' ||
'rottenTomatoesCritic' => RatingInfo(value >= _rottenTomatoesFresh ? _rtFreshAsset : _rtRottenAsset, _percent(value)),
'rottenTomatoesAudience' => RatingInfo(
value >= _rottenTomatoesFresh ? _rtUprightAsset : _rtSpilledAsset,
_percent(value),
),
_ => null,
};
const String _imdbAsset = 'assets/rating_icons/imdb.svg';
const String _tmdbAsset = 'assets/rating_icons/tmdb.svg';
const String _rtFreshAsset = 'assets/rating_icons/rt_fresh.svg';
const String _rtRottenAsset = 'assets/rating_icons/rt_rotten.svg';
const String _rtUprightAsset = 'assets/rating_icons/rt_upright.svg';
const String _rtSpilledAsset = 'assets/rating_icons/rt_spilled.svg';
/// Ratings are normalized to a 0-10 scale; Rotten Tomatoes is a percentage.
const double _rottenTomatoesFresh = 6.0;
String _percent(double value) => '${(value * 10).toStringAsFixed(0)}%';