@@ -5,6 +5,7 @@ import '../media/media_kind.dart';
|
|||||||
import '../media/media_server_client.dart';
|
import '../media/media_server_client.dart';
|
||||||
import '../services/jellyfin_client.dart';
|
import '../services/jellyfin_client.dart';
|
||||||
import '../utils/jellyfin_time.dart';
|
import '../utils/jellyfin_time.dart';
|
||||||
|
import '../utils/media_image_helper.dart';
|
||||||
import 'metadata_edit_models.dart';
|
import 'metadata_edit_models.dart';
|
||||||
|
|
||||||
class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
|
class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
|
||||||
@@ -235,11 +236,32 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
|
|
||||||
List<MetadataEditField> _artworkFields(MediaKind kind) {
|
List<MetadataEditField> _artworkFields(MediaKind kind) {
|
||||||
final fields = <MetadataEditField>[
|
final fields = <MetadataEditField>[
|
||||||
_artworkField('Primary', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3),
|
// Episode "posters" are 16:9 thumbnails, not 2:3 poster art.
|
||||||
|
kind == MediaKind.episode
|
||||||
|
? _artworkField(
|
||||||
|
'Primary',
|
||||||
|
t.metadataEdit.poster,
|
||||||
|
t.metadataEdit.selectPoster,
|
||||||
|
80,
|
||||||
|
45,
|
||||||
|
2,
|
||||||
|
16 / 9,
|
||||||
|
imageType: ImageType.thumb,
|
||||||
|
)
|
||||||
|
: _artworkField('Primary', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3),
|
||||||
];
|
];
|
||||||
if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.episode) {
|
if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.episode) {
|
||||||
fields.add(
|
fields.add(
|
||||||
_artworkField('Backdrop', t.metadataEdit.background, t.metadataEdit.selectBackground, 80, 45, 2, 16 / 9),
|
_artworkField(
|
||||||
|
'Backdrop',
|
||||||
|
t.metadataEdit.background,
|
||||||
|
t.metadataEdit.selectBackground,
|
||||||
|
80,
|
||||||
|
45,
|
||||||
|
2,
|
||||||
|
16 / 9,
|
||||||
|
imageType: ImageType.art,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (kind == MediaKind.movie || kind == MediaKind.show) {
|
if (kind == MediaKind.movie || kind == MediaKind.show) {
|
||||||
@@ -253,6 +275,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
2,
|
2,
|
||||||
2.5,
|
2.5,
|
||||||
fit: MetadataArtworkFit.contain,
|
fit: MetadataArtworkFit.contain,
|
||||||
|
imageType: ImageType.logo,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -268,6 +291,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
int columns,
|
int columns,
|
||||||
double aspectRatio, {
|
double aspectRatio, {
|
||||||
MetadataArtworkFit fit = MetadataArtworkFit.cover,
|
MetadataArtworkFit fit = MetadataArtworkFit.cover,
|
||||||
|
ImageType imageType = ImageType.poster,
|
||||||
}) {
|
}) {
|
||||||
return MetadataEditField(
|
return MetadataEditField(
|
||||||
id: 'artwork:$key',
|
id: 'artwork:$key',
|
||||||
@@ -282,6 +306,7 @@ class JellyfinMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
gridColumns: columns,
|
gridColumns: columns,
|
||||||
gridAspectRatio: aspectRatio,
|
gridAspectRatio: aspectRatio,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
imageType: imageType,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import '../media/media_backend.dart';
|
|||||||
import '../media/media_item.dart';
|
import '../media/media_item.dart';
|
||||||
import '../media/media_kind.dart';
|
import '../media/media_kind.dart';
|
||||||
import '../media/media_server_client.dart';
|
import '../media/media_server_client.dart';
|
||||||
|
import '../utils/media_image_helper.dart';
|
||||||
|
|
||||||
enum MetadataEditFieldType { text, multilineText, date, stringList, choice, artwork }
|
enum MetadataEditFieldType { text, multilineText, date, stringList, choice, artwork }
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ class MetadataArtworkConfig {
|
|||||||
final int gridColumns;
|
final int gridColumns;
|
||||||
final double gridAspectRatio;
|
final double gridAspectRatio;
|
||||||
final MetadataArtworkFit fit;
|
final MetadataArtworkFit fit;
|
||||||
|
final ImageType imageType;
|
||||||
|
|
||||||
const MetadataArtworkConfig({
|
const MetadataArtworkConfig({
|
||||||
required this.key,
|
required this.key,
|
||||||
@@ -33,6 +35,7 @@ class MetadataArtworkConfig {
|
|||||||
required this.gridColumns,
|
required this.gridColumns,
|
||||||
required this.gridAspectRatio,
|
required this.gridAspectRatio,
|
||||||
this.fit = MetadataArtworkFit.cover,
|
this.fit = MetadataArtworkFit.cover,
|
||||||
|
this.imageType = ImageType.poster,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../media/media_kind.dart';
|
|||||||
import '../media/media_server_client.dart';
|
import '../media/media_server_client.dart';
|
||||||
import '../services/plex_client.dart';
|
import '../services/plex_client.dart';
|
||||||
import '../utils/language_codes.dart';
|
import '../utils/language_codes.dart';
|
||||||
|
import '../utils/media_image_helper.dart';
|
||||||
import 'metadata_edit_models.dart';
|
import 'metadata_edit_models.dart';
|
||||||
|
|
||||||
class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
||||||
@@ -240,10 +241,33 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
|
|
||||||
List<MetadataEditField> _artworkFields(MediaKind kind) {
|
List<MetadataEditField> _artworkFields(MediaKind kind) {
|
||||||
final fields = <MetadataEditField>[
|
final fields = <MetadataEditField>[
|
||||||
_artworkField('posters', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3),
|
// Episode "posters" are 16:9 thumbnails, not 2:3 poster art.
|
||||||
|
kind == MediaKind.episode
|
||||||
|
? _artworkField(
|
||||||
|
'posters',
|
||||||
|
t.metadataEdit.poster,
|
||||||
|
t.metadataEdit.selectPoster,
|
||||||
|
80,
|
||||||
|
45,
|
||||||
|
2,
|
||||||
|
16 / 9,
|
||||||
|
imageType: ImageType.thumb,
|
||||||
|
)
|
||||||
|
: _artworkField('posters', t.metadataEdit.poster, t.metadataEdit.selectPoster, 40, 60, 3, 2 / 3),
|
||||||
];
|
];
|
||||||
if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.episode) {
|
if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.episode) {
|
||||||
fields.add(_artworkField('arts', t.metadataEdit.background, t.metadataEdit.selectBackground, 80, 45, 2, 16 / 9));
|
fields.add(
|
||||||
|
_artworkField(
|
||||||
|
'arts',
|
||||||
|
t.metadataEdit.background,
|
||||||
|
t.metadataEdit.selectBackground,
|
||||||
|
80,
|
||||||
|
45,
|
||||||
|
2,
|
||||||
|
16 / 9,
|
||||||
|
imageType: ImageType.art,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.collection) {
|
if (kind == MediaKind.movie || kind == MediaKind.show || kind == MediaKind.collection) {
|
||||||
fields.add(
|
fields.add(
|
||||||
@@ -256,9 +280,21 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
2,
|
2,
|
||||||
2.5,
|
2.5,
|
||||||
fit: MetadataArtworkFit.contain,
|
fit: MetadataArtworkFit.contain,
|
||||||
|
imageType: ImageType.logo,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
fields.add(
|
||||||
|
_artworkField(
|
||||||
|
'squareArts',
|
||||||
|
t.metadataEdit.squareArt,
|
||||||
|
t.metadataEdit.selectSquareArt,
|
||||||
|
50,
|
||||||
|
50,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
imageType: ImageType.avatar,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
fields.add(_artworkField('squareArts', t.metadataEdit.squareArt, t.metadataEdit.selectSquareArt, 50, 50, 3, 1));
|
|
||||||
}
|
}
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
@@ -272,6 +308,7 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
int columns,
|
int columns,
|
||||||
double aspectRatio, {
|
double aspectRatio, {
|
||||||
MetadataArtworkFit fit = MetadataArtworkFit.cover,
|
MetadataArtworkFit fit = MetadataArtworkFit.cover,
|
||||||
|
ImageType imageType = ImageType.poster,
|
||||||
}) {
|
}) {
|
||||||
return MetadataEditField(
|
return MetadataEditField(
|
||||||
id: 'artwork:$key',
|
id: 'artwork:$key',
|
||||||
@@ -286,6 +323,7 @@ class PlexMetadataEditAdapter extends MetadataEditAdapter {
|
|||||||
gridColumns: columns,
|
gridColumns: columns,
|
||||||
gridAspectRatio: aspectRatio,
|
gridAspectRatio: aspectRatio,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
imageType: imageType,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import '../services/file_picker_service.dart';
|
|||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
import '../utils/dialogs.dart';
|
import '../utils/dialogs.dart';
|
||||||
import '../utils/formatters.dart';
|
import '../utils/formatters.dart';
|
||||||
import '../utils/media_image_helper.dart';
|
|
||||||
import '../utils/provider_extensions.dart';
|
import '../utils/provider_extensions.dart';
|
||||||
import '../utils/snackbar_helper.dart';
|
import '../utils/snackbar_helper.dart';
|
||||||
import '../widgets/app_icon.dart';
|
import '../widgets/app_icon.dart';
|
||||||
@@ -373,7 +372,7 @@ class _MetadataEditScreenState extends State<MetadataEditScreen> {
|
|||||||
width: artwork.previewWidth,
|
width: artwork.previewWidth,
|
||||||
height: artwork.previewHeight,
|
height: artwork.previewHeight,
|
||||||
fit: artwork.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover,
|
fit: artwork.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover,
|
||||||
imageType: _imageTypeForArtwork(artwork),
|
imageType: artwork.imageType,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -549,7 +548,7 @@ class _ArtworkPickerDialogState extends State<ArtworkPickerDialog> {
|
|||||||
client: widget.adapter.mediaClient,
|
client: widget.adapter.mediaClient,
|
||||||
imagePath: artwork.thumbnailPath,
|
imagePath: artwork.thumbnailPath,
|
||||||
fit: _config.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover,
|
fit: _config.fit == MetadataArtworkFit.contain ? BoxFit.contain : BoxFit.cover,
|
||||||
imageType: _imageTypeForArtwork(_config),
|
imageType: _config.imageType,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -571,11 +570,3 @@ class _ArtworkPickerDialogState extends State<ArtworkPickerDialog> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageType _imageTypeForArtwork(MetadataArtworkConfig artwork) {
|
|
||||||
final key = artwork.key.toLowerCase();
|
|
||||||
if (key == 'arts' || key == 'backdrop') return ImageType.art;
|
|
||||||
if (key == 'clearlogos' || key == 'logo') return ImageType.logo;
|
|
||||||
if (key == 'squarearts') return ImageType.avatar;
|
|
||||||
return ImageType.poster;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import 'package:plezy/media/media_backend.dart';
|
|||||||
import 'package:plezy/media/media_item.dart';
|
import 'package:plezy/media/media_item.dart';
|
||||||
import 'package:plezy/media/media_kind.dart';
|
import 'package:plezy/media/media_kind.dart';
|
||||||
import 'package:plezy/metadata_edit/jellyfin_metadata_edit_adapter.dart';
|
import 'package:plezy/metadata_edit/jellyfin_metadata_edit_adapter.dart';
|
||||||
|
import 'package:plezy/metadata_edit/metadata_edit_models.dart';
|
||||||
import 'package:plezy/services/jellyfin_client.dart';
|
import 'package:plezy/services/jellyfin_client.dart';
|
||||||
|
import 'package:plezy/utils/media_image_helper.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('load fails when the full editable Jellyfin DTO is unavailable', () async {
|
test('load fails when the full editable Jellyfin DTO is unavailable', () async {
|
||||||
@@ -29,6 +31,31 @@ void main() {
|
|||||||
expect(adapter.load(item), throwsA(isA<StateError>()));
|
expect(adapter.load(item), throwsA(isA<StateError>()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('episode poster artwork uses 16:9 thumbnail geometry', () async {
|
||||||
|
final client = JellyfinClient.forTesting(
|
||||||
|
connection: _connection(),
|
||||||
|
httpClient: MockClient((request) async => http.Response('', 500)),
|
||||||
|
);
|
||||||
|
addTearDown(client.close);
|
||||||
|
final adapter = JellyfinMetadataEditAdapter(client);
|
||||||
|
|
||||||
|
MetadataArtworkConfig posterConfig(MediaKind kind) {
|
||||||
|
final item = MediaItem(id: 'item-1', backend: MediaBackend.jellyfin, kind: kind);
|
||||||
|
final draft = MetadataEditDraft(sourceItem: item, currentItem: item, values: {});
|
||||||
|
final artwork = adapter.buildSchema(draft).singleWhere((section) => section.id == 'artwork');
|
||||||
|
return artwork.fields.singleWhere((field) => field.id == 'artwork:Primary').artwork!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final episode = posterConfig(MediaKind.episode);
|
||||||
|
expect(episode.gridAspectRatio, 16 / 9);
|
||||||
|
expect(episode.imageType, ImageType.thumb);
|
||||||
|
expect(episode.gridColumns, 2);
|
||||||
|
|
||||||
|
final movie = posterConfig(MediaKind.movie);
|
||||||
|
expect(movie.gridAspectRatio, 2 / 3);
|
||||||
|
expect(movie.imageType, ImageType.poster);
|
||||||
|
});
|
||||||
|
|
||||||
test('save preserves unchanged Jellyfin people and studio identity data', () async {
|
test('save preserves unchanged Jellyfin people and studio identity data', () async {
|
||||||
String? capturedBody;
|
String? capturedBody;
|
||||||
final client = JellyfinClient.forTesting(
|
final client = JellyfinClient.forTesting(
|
||||||
|
|||||||
Reference in New Issue
Block a user