Spaces:
Paused
Paused
PNRxA commited on
Commit ·
9052d8e
1
Parent(s): 4f091a3
fix: pills always render as a horizontal lozenge + a bit more top/bottom padding
Browse files- A pill is a horizontal icon/label-left, value-right lozenge; vertical
stacked pills look wrong, so pill shape now forces horizontal layout for
every image type (logos/backdrops/episodes default to vertical style).
- Give horizontal pills a little extra height for top/bottom breathing room.
- README.md +1 -1
- api/src/image/badge.rs +26 -15
- api/src/image/generate.rs +38 -0
- api/src/services/db.rs +9 -0
README.md
CHANGED
|
@@ -128,7 +128,7 @@ GET /{api_key}/isValid
|
|
| 128 |
- `?badge_style={h|v|d}`: badge layout — `h` (horizontal), `v` (vertical), `d` (default)
|
| 129 |
- `?label_style={t|i|o}`: label rendering — `t` (text), `i` (icon), `o` (official provider logos)
|
| 130 |
- `?badge_size={xs|s|m|l|xl}`: badge scale — extra-small, small, medium, large, extra-large
|
| 131 |
-
- `?badge_shape={r|p}`: badge corner shape — `r` (rounded, default), `p` (pill, fully rounded ends)
|
| 132 |
- `?badge_background={d|k|t|n}`: badge background — `d` (default: source-coloured label + dark value), `k` (dark: uniformly dark), `t` (transparent: semi-transparent so the artwork shows through), `n` (none: no background, label/value drawn directly on the image with a drop shadow)
|
| 133 |
- `?image_source={t|f}`: image source — `t` (TMDB, default), `f` (Fanart.tv). Applies to all image types. The non-selected source is used as fallback
|
| 134 |
- `?badge_direction={d|h|v}`: badge stacking direction — `d` (default), `h` (horizontal), `v` (vertical). Applies to poster, backdrop, and episode endpoints
|
|
|
|
| 128 |
- `?badge_style={h|v|d}`: badge layout — `h` (horizontal), `v` (vertical), `d` (default)
|
| 129 |
- `?label_style={t|i|o}`: label rendering — `t` (text), `i` (icon), `o` (official provider logos)
|
| 130 |
- `?badge_size={xs|s|m|l|xl}`: badge scale — extra-small, small, medium, large, extra-large
|
| 131 |
+
- `?badge_shape={r|p}`: badge corner shape — `r` (rounded, default), `p` (pill, fully rounded ends). Pills always render as a horizontal icon/label-left, value-right lozenge, even on image types whose default style is vertical (logos, backdrops, episodes)
|
| 132 |
- `?badge_background={d|k|t|n}`: badge background — `d` (default: source-coloured label + dark value), `k` (dark: uniformly dark), `t` (transparent: semi-transparent so the artwork shows through), `n` (none: no background, label/value drawn directly on the image with a drop shadow)
|
| 133 |
- `?image_source={t|f}`: image source — `t` (TMDB, default), `f` (Fanart.tv). Applies to all image types. The non-selected source is used as fallback
|
| 134 |
- `?badge_direction={d|h|v}`: badge stacking direction — `d` (default), `h` (horizontal), `v` (vertical). Applies to poster, backdrop, and episode endpoints
|
api/src/image/badge.rs
CHANGED
|
@@ -105,6 +105,9 @@ const BASE_BADGE_RADIUS: u32 = 10;
|
|
| 105 |
/// crowd the fully-rounded caps. Applied to the long-axis ends only (left/right
|
| 106 |
/// for horizontal badges, top/bottom for vertical).
|
| 107 |
const BASE_PILL_PADDING: u32 = 10;
|
|
|
|
|
|
|
|
|
|
| 108 |
const BASE_FONT_SIZE: f32 = 34.0;
|
| 109 |
const BASE_LABEL_FONT_SIZE: f32 = 26.0;
|
| 110 |
const BASE_ICON_HEIGHT: u32 = 48;
|
|
@@ -164,6 +167,7 @@ struct ScaledDims {
|
|
| 164 |
badge_value_padding_h: u32,
|
| 165 |
badge_radius: u32,
|
| 166 |
pill_padding: u32,
|
|
|
|
| 167 |
icon_height: u32,
|
| 168 |
}
|
| 169 |
|
|
@@ -176,6 +180,7 @@ impl ScaledDims {
|
|
| 176 |
badge_value_padding_h: (BASE_BADGE_VALUE_PADDING_H as f32 * badge_scale).round() as u32,
|
| 177 |
badge_radius: (BASE_BADGE_RADIUS as f32 * badge_scale).round() as u32,
|
| 178 |
pill_padding: (BASE_PILL_PADDING as f32 * badge_scale).round() as u32,
|
|
|
|
| 179 |
icon_height: (BASE_ICON_HEIGHT as f32 * badge_scale).round() as u32,
|
| 180 |
}
|
| 181 |
}
|
|
@@ -289,26 +294,28 @@ fn render_badge_inner(
|
|
| 289 |
let value_width = uniform_value_width.unwrap_or_else(|| text_width(value, &fonts.scaled));
|
| 290 |
let label_pad = if use_icon { dims.badge_padding_h } else { dims.text_label_padding_h };
|
| 291 |
// Pills get extra padding at their rounded left/right ends so the content
|
| 292 |
-
// doesn't crowd the fully-rounded caps
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
BadgeShape::
|
|
|
|
| 296 |
};
|
|
|
|
| 297 |
|
| 298 |
let value_x = pill_pad + label_width + label_pad * 2;
|
| 299 |
let total_width = value_x + value_width + dims.badge_value_padding_h + dims.badge_value_padding_h / 2 + 2 + pill_pad;
|
| 300 |
-
let mut img = RgbaImage::new(total_width,
|
| 301 |
|
| 302 |
// Draw the label + value backgrounds (unless the badge has no background).
|
| 303 |
// Both sections are filled as plain rects, then the four outer corners are
|
| 304 |
// rounded — leaving the inner join square for a clean seam at any radius.
|
| 305 |
if let Some((label_bg, value_bg)) = section_colors(appearance.background, label_style, &badge.source) {
|
| 306 |
-
draw_filled_rect_mut(&mut img, Rect::at(0, 0).of_size(value_x,
|
| 307 |
-
draw_filled_rect_mut(&mut img, Rect::at(value_x as i32, 0).of_size(total_width - value_x,
|
| 308 |
-
round_corners(&mut img, corner_radius(appearance.shape,
|
| 309 |
}
|
| 310 |
|
| 311 |
-
let shadow = shadow_offset(appearance.background,
|
| 312 |
|
| 313 |
// Draw label (icon or text, centered within uniform label area)
|
| 314 |
if use_icon {
|
|
@@ -319,12 +326,12 @@ fn render_badge_inner(
|
|
| 319 |
imageops::resize(icon, icon_w, icon_h, imageops::FilterType::Lanczos3)
|
| 320 |
};
|
| 321 |
let ix = pill_pad + label_pad + (label_width.saturating_sub(icon_w)) / 2;
|
| 322 |
-
let iy = (
|
| 323 |
overlay_icon_shadowed(&mut img, &scaled_icon, ix as i64, iy as i64, shadow);
|
| 324 |
} else {
|
| 325 |
let actual_label_width = text_width(label, &fonts.label_scaled);
|
| 326 |
let label_x = pill_pad + label_pad + (label_width.saturating_sub(actual_label_width)) / 2;
|
| 327 |
-
let label_y = (
|
| 328 |
draw_text_shadowed(
|
| 329 |
&mut img,
|
| 330 |
Rgba([255, 255, 255, 255]),
|
|
@@ -340,7 +347,7 @@ fn render_badge_inner(
|
|
| 340 |
// Draw value text (centered within uniform value area)
|
| 341 |
let actual_value_width = text_width(value, &fonts.scaled);
|
| 342 |
let value_text_x = value_x + dims.badge_value_padding_h + (value_width.saturating_sub(actual_value_width)) / 2;
|
| 343 |
-
let value_y = (
|
| 344 |
draw_text_shadowed(
|
| 345 |
&mut img,
|
| 346 |
Rgba([255, 255, 255, 255]),
|
|
@@ -687,13 +694,17 @@ mod tests {
|
|
| 687 |
for shape in ALL_SHAPES {
|
| 688 |
for background in ALL_BACKGROUNDS {
|
| 689 |
let appearance = BadgeAppearance { shape, background };
|
| 690 |
-
// Horizontal
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
let h = render_badge_appearance(&badge, &font, LabelStyle::Text, appearance);
|
| 692 |
-
assert_eq!(h.height(),
|
| 693 |
assert!(h.width() > 0, "h width for {shape:?}/{background:?}");
|
| 694 |
// Icon labels exercise the icon/shadow path.
|
| 695 |
let hi = render_badge_appearance(&badge, &font, LabelStyle::Official, appearance);
|
| 696 |
-
assert_eq!(hi.height(),
|
| 697 |
// Vertical: appearance must not change the badge width.
|
| 698 |
let v = render_vertical_badge(&badge, &font, LabelStyle::Text, appearance, 1.0);
|
| 699 |
assert_eq!(v.width(), BASE_VERT_BADGE_WIDTH, "v width for {shape:?}/{background:?}");
|
|
|
|
| 105 |
/// crowd the fully-rounded caps. Applied to the long-axis ends only (left/right
|
| 106 |
/// for horizontal badges, top/bottom for vertical).
|
| 107 |
const BASE_PILL_PADDING: u32 = 10;
|
| 108 |
+
/// Extra height added to horizontal pill badges for a little more top/bottom
|
| 109 |
+
/// breathing room around the content.
|
| 110 |
+
const BASE_PILL_PADDING_V: u32 = 6;
|
| 111 |
const BASE_FONT_SIZE: f32 = 34.0;
|
| 112 |
const BASE_LABEL_FONT_SIZE: f32 = 26.0;
|
| 113 |
const BASE_ICON_HEIGHT: u32 = 48;
|
|
|
|
| 167 |
badge_value_padding_h: u32,
|
| 168 |
badge_radius: u32,
|
| 169 |
pill_padding: u32,
|
| 170 |
+
pill_padding_v: u32,
|
| 171 |
icon_height: u32,
|
| 172 |
}
|
| 173 |
|
|
|
|
| 180 |
badge_value_padding_h: (BASE_BADGE_VALUE_PADDING_H as f32 * badge_scale).round() as u32,
|
| 181 |
badge_radius: (BASE_BADGE_RADIUS as f32 * badge_scale).round() as u32,
|
| 182 |
pill_padding: (BASE_PILL_PADDING as f32 * badge_scale).round() as u32,
|
| 183 |
+
pill_padding_v: (BASE_PILL_PADDING_V as f32 * badge_scale).round() as u32,
|
| 184 |
icon_height: (BASE_ICON_HEIGHT as f32 * badge_scale).round() as u32,
|
| 185 |
}
|
| 186 |
}
|
|
|
|
| 294 |
let value_width = uniform_value_width.unwrap_or_else(|| text_width(value, &fonts.scaled));
|
| 295 |
let label_pad = if use_icon { dims.badge_padding_h } else { dims.text_label_padding_h };
|
| 296 |
// Pills get extra padding at their rounded left/right ends so the content
|
| 297 |
+
// doesn't crowd the fully-rounded caps, plus a little extra height for
|
| 298 |
+
// top/bottom breathing room. Rounded badges add nothing.
|
| 299 |
+
let (pill_pad, pill_pad_v) = match appearance.shape {
|
| 300 |
+
BadgeShape::Pill => (dims.pill_padding, dims.pill_padding_v),
|
| 301 |
+
BadgeShape::Rounded => (0, 0),
|
| 302 |
};
|
| 303 |
+
let badge_h = dims.badge_height + pill_pad_v;
|
| 304 |
|
| 305 |
let value_x = pill_pad + label_width + label_pad * 2;
|
| 306 |
let total_width = value_x + value_width + dims.badge_value_padding_h + dims.badge_value_padding_h / 2 + 2 + pill_pad;
|
| 307 |
+
let mut img = RgbaImage::new(total_width, badge_h);
|
| 308 |
|
| 309 |
// Draw the label + value backgrounds (unless the badge has no background).
|
| 310 |
// Both sections are filled as plain rects, then the four outer corners are
|
| 311 |
// rounded — leaving the inner join square for a clean seam at any radius.
|
| 312 |
if let Some((label_bg, value_bg)) = section_colors(appearance.background, label_style, &badge.source) {
|
| 313 |
+
draw_filled_rect_mut(&mut img, Rect::at(0, 0).of_size(value_x, badge_h), label_bg);
|
| 314 |
+
draw_filled_rect_mut(&mut img, Rect::at(value_x as i32, 0).of_size(total_width - value_x, badge_h), value_bg);
|
| 315 |
+
round_corners(&mut img, corner_radius(appearance.shape, badge_h, dims.badge_radius));
|
| 316 |
}
|
| 317 |
|
| 318 |
+
let shadow = shadow_offset(appearance.background, badge_h);
|
| 319 |
|
| 320 |
// Draw label (icon or text, centered within uniform label area)
|
| 321 |
if use_icon {
|
|
|
|
| 326 |
imageops::resize(icon, icon_w, icon_h, imageops::FilterType::Lanczos3)
|
| 327 |
};
|
| 328 |
let ix = pill_pad + label_pad + (label_width.saturating_sub(icon_w)) / 2;
|
| 329 |
+
let iy = (badge_h.saturating_sub(icon_h)) / 2;
|
| 330 |
overlay_icon_shadowed(&mut img, &scaled_icon, ix as i64, iy as i64, shadow);
|
| 331 |
} else {
|
| 332 |
let actual_label_width = text_width(label, &fonts.label_scaled);
|
| 333 |
let label_x = pill_pad + label_pad + (label_width.saturating_sub(actual_label_width)) / 2;
|
| 334 |
+
let label_y = (badge_h as i32 - fonts.label_scale.x as i32) / 2;
|
| 335 |
draw_text_shadowed(
|
| 336 |
&mut img,
|
| 337 |
Rgba([255, 255, 255, 255]),
|
|
|
|
| 347 |
// Draw value text (centered within uniform value area)
|
| 348 |
let actual_value_width = text_width(value, &fonts.scaled);
|
| 349 |
let value_text_x = value_x + dims.badge_value_padding_h + (value_width.saturating_sub(actual_value_width)) / 2;
|
| 350 |
+
let value_y = (badge_h as i32 - fonts.scale.x as i32) / 2;
|
| 351 |
draw_text_shadowed(
|
| 352 |
&mut img,
|
| 353 |
Rgba([255, 255, 255, 255]),
|
|
|
|
| 694 |
for shape in ALL_SHAPES {
|
| 695 |
for background in ALL_BACKGROUNDS {
|
| 696 |
let appearance = BadgeAppearance { shape, background };
|
| 697 |
+
// Horizontal badge height: base, plus the pill's extra vertical padding.
|
| 698 |
+
let expected_h = match shape {
|
| 699 |
+
BadgeShape::Pill => BASE_BADGE_HEIGHT + BASE_PILL_PADDING_V,
|
| 700 |
+
BadgeShape::Rounded => BASE_BADGE_HEIGHT,
|
| 701 |
+
};
|
| 702 |
let h = render_badge_appearance(&badge, &font, LabelStyle::Text, appearance);
|
| 703 |
+
assert_eq!(h.height(), expected_h, "h height for {shape:?}/{background:?}");
|
| 704 |
assert!(h.width() > 0, "h width for {shape:?}/{background:?}");
|
| 705 |
// Icon labels exercise the icon/shadow path.
|
| 706 |
let hi = render_badge_appearance(&badge, &font, LabelStyle::Official, appearance);
|
| 707 |
+
assert_eq!(hi.height(), expected_h, "h-icon height for {shape:?}/{background:?}");
|
| 708 |
// Vertical: appearance must not change the badge width.
|
| 709 |
let v = render_vertical_badge(&badge, &font, LabelStyle::Text, appearance, 1.0);
|
| 710 |
assert_eq!(v.width(), BASE_VERT_BADGE_WIDTH, "v width for {shape:?}/{background:?}");
|
api/src/image/generate.rs
CHANGED
|
@@ -278,6 +278,9 @@ pub fn render_poster_sync(
|
|
| 278 |
badge_size: BadgeSize,
|
| 279 |
poster_badge_split: bool,
|
| 280 |
) -> Result<Vec<u8>, AppError> {
|
|
|
|
|
|
|
|
|
|
| 281 |
let base = load_image_with_limits(poster_bytes)?;
|
| 282 |
|
| 283 |
let base = if base.width() != target_width {
|
|
@@ -364,6 +367,9 @@ pub fn render_logo_sync(
|
|
| 364 |
target_width: u32,
|
| 365 |
badge_scale: f32,
|
| 366 |
) -> Result<Vec<u8>, AppError> {
|
|
|
|
|
|
|
|
|
|
| 367 |
let base = load_image_with_limits(logo_bytes)?;
|
| 368 |
|
| 369 |
let base = if base.width() != target_width {
|
|
@@ -547,6 +553,9 @@ pub fn render_backdrop_sync(
|
|
| 547 |
badge_scale: f32,
|
| 548 |
_badge_size: BadgeSize,
|
| 549 |
) -> Result<Vec<u8>, AppError> {
|
|
|
|
|
|
|
|
|
|
| 550 |
let base = load_image_with_limits(backdrop_bytes)?;
|
| 551 |
|
| 552 |
let base = if base.width() != target_width {
|
|
@@ -631,6 +640,9 @@ pub fn render_episode_sync(
|
|
| 631 |
badge_size: BadgeSize,
|
| 632 |
blur: bool,
|
| 633 |
) -> Result<Vec<u8>, AppError> {
|
|
|
|
|
|
|
|
|
|
| 634 |
let base = image::load_from_memory(image_bytes)
|
| 635 |
.map_err(AppError::Image)?;
|
| 636 |
|
|
@@ -801,6 +813,32 @@ mod tests {
|
|
| 801 |
assert_eq!(&result[..4], &[0x89, b'P', b'N', b'G']);
|
| 802 |
}
|
| 803 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
#[test]
|
| 805 |
fn render_logo_downscales_wide_image() {
|
| 806 |
let font = FontArc::try_from_slice(crate::FONT_BYTES).unwrap();
|
|
|
|
| 278 |
badge_size: BadgeSize,
|
| 279 |
poster_badge_split: bool,
|
| 280 |
) -> Result<Vec<u8>, AppError> {
|
| 281 |
+
// A pill is a horizontal lozenge (icon/label left, value right) — never a
|
| 282 |
+
// vertical stacked badge, even when the configured style is vertical.
|
| 283 |
+
let badge_style = if badge_appearance.is_pill() { BadgeStyle::Horizontal } else { badge_style };
|
| 284 |
let base = load_image_with_limits(poster_bytes)?;
|
| 285 |
|
| 286 |
let base = if base.width() != target_width {
|
|
|
|
| 367 |
target_width: u32,
|
| 368 |
badge_scale: f32,
|
| 369 |
) -> Result<Vec<u8>, AppError> {
|
| 370 |
+
// A pill is a horizontal lozenge (icon/label left, value right) — never a
|
| 371 |
+
// vertical stacked badge, even when the configured style is vertical.
|
| 372 |
+
let badge_style = if badge_appearance.is_pill() { BadgeStyle::Horizontal } else { badge_style };
|
| 373 |
let base = load_image_with_limits(logo_bytes)?;
|
| 374 |
|
| 375 |
let base = if base.width() != target_width {
|
|
|
|
| 553 |
badge_scale: f32,
|
| 554 |
_badge_size: BadgeSize,
|
| 555 |
) -> Result<Vec<u8>, AppError> {
|
| 556 |
+
// A pill is a horizontal lozenge (icon/label left, value right) — never a
|
| 557 |
+
// vertical stacked badge, even when the configured style is vertical.
|
| 558 |
+
let badge_style = if badge_appearance.is_pill() { BadgeStyle::Horizontal } else { badge_style };
|
| 559 |
let base = load_image_with_limits(backdrop_bytes)?;
|
| 560 |
|
| 561 |
let base = if base.width() != target_width {
|
|
|
|
| 640 |
badge_size: BadgeSize,
|
| 641 |
blur: bool,
|
| 642 |
) -> Result<Vec<u8>, AppError> {
|
| 643 |
+
// A pill is a horizontal lozenge (icon/label left, value right) — never a
|
| 644 |
+
// vertical stacked badge, even when the configured style is vertical.
|
| 645 |
+
let badge_style = if badge_appearance.is_pill() { BadgeStyle::Horizontal } else { badge_style };
|
| 646 |
let base = image::load_from_memory(image_bytes)
|
| 647 |
.map_err(AppError::Image)?;
|
| 648 |
|
|
|
|
| 813 |
assert_eq!(&result[..4], &[0x89, b'P', b'N', b'G']);
|
| 814 |
}
|
| 815 |
|
| 816 |
+
#[test]
|
| 817 |
+
fn pill_renders_horizontally_regardless_of_style() {
|
| 818 |
+
use crate::services::db::{BadgeBackground, BadgeShape};
|
| 819 |
+
use crate::services::ratings::{RatingBadge, RatingSource};
|
| 820 |
+
|
| 821 |
+
let font = FontArc::try_from_slice(crate::FONT_BYTES).unwrap();
|
| 822 |
+
let png = test_png(400, 100);
|
| 823 |
+
let badges = vec![
|
| 824 |
+
RatingBadge { source: RatingSource::Imdb, value: "8.5".to_string() },
|
| 825 |
+
RatingBadge { source: RatingSource::Tmdb, value: "85%".to_string() },
|
| 826 |
+
];
|
| 827 |
+
let pill = BadgeAppearance { shape: BadgeShape::Pill, background: BadgeBackground::Default };
|
| 828 |
+
let rounded = BadgeAppearance::default();
|
| 829 |
+
|
| 830 |
+
// A pill ignores the vertical style and always renders horizontally, so
|
| 831 |
+
// vertical+pill and horizontal+pill produce identical output.
|
| 832 |
+
let v_pill = render_logo_sync(&png, &badges, &font, BadgeStyle::Vertical, LabelStyle::Official, pill, 500, 1.0).unwrap();
|
| 833 |
+
let h_pill = render_logo_sync(&png, &badges, &font, BadgeStyle::Horizontal, LabelStyle::Official, pill, 500, 1.0).unwrap();
|
| 834 |
+
assert_eq!(v_pill, h_pill, "pill should render horizontally even when the style is vertical");
|
| 835 |
+
|
| 836 |
+
// Rounded badges still honour the style, so the two layouts differ.
|
| 837 |
+
let v_round = render_logo_sync(&png, &badges, &font, BadgeStyle::Vertical, LabelStyle::Official, rounded, 500, 1.0).unwrap();
|
| 838 |
+
let h_round = render_logo_sync(&png, &badges, &font, BadgeStyle::Horizontal, LabelStyle::Official, rounded, 500, 1.0).unwrap();
|
| 839 |
+
assert_ne!(v_round, h_round, "rounded badges should still differ between vertical and horizontal styles");
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
#[test]
|
| 843 |
fn render_logo_downscales_wide_image() {
|
| 844 |
let font = FontArc::try_from_slice(crate::FONT_BYTES).unwrap();
|
api/src/services/db.rs
CHANGED
|
@@ -290,6 +290,15 @@ impl Default for BadgeAppearance {
|
|
| 290 |
}
|
| 291 |
}
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 294 |
pub enum ImageSource {
|
| 295 |
Tmdb,
|
|
|
|
| 290 |
}
|
| 291 |
}
|
| 292 |
|
| 293 |
+
impl BadgeAppearance {
|
| 294 |
+
/// A pill is a horizontal lozenge (icon/label left, value right). Vertical
|
| 295 |
+
/// stacked pills look wrong, so callers render pills horizontally regardless
|
| 296 |
+
/// of the configured badge style.
|
| 297 |
+
pub fn is_pill(self) -> bool {
|
| 298 |
+
matches!(self.shape, BadgeShape::Pill)
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 303 |
pub enum ImageSource {
|
| 304 |
Tmdb,
|