refactor: share focus chrome and simplify the TV picker and browse paths
Focus chrome was implemented twice, once in the focusable wrapper and once in the focus builders; both now go through FocusChrome. TvColorPicker's channel row was a copy of TvNumberSpinner and is now that widget in compact density. Also trims unused helpers and fields and simplifies the Jellyfin browse paths.
This commit is contained in:
@@ -95,21 +95,18 @@ void main() {
|
||||
final movie = _movie(artPath: '/art', backgroundSquarePath: '/square');
|
||||
|
||||
expect(movie.heroArtCandidates(containerAspectRatio: 1.0), ['/square', '/art']);
|
||||
expect(movie.heroArt(containerAspectRatio: 1.0), '/square');
|
||||
});
|
||||
|
||||
test('near-square containers fall back to wide cover art when square art is missing', () {
|
||||
final movie = _movie(artPath: '/art');
|
||||
|
||||
expect(movie.heroArtCandidates(containerAspectRatio: 1.0), ['/art']);
|
||||
expect(movie.heroArt(containerAspectRatio: 1.0), '/art');
|
||||
});
|
||||
|
||||
test('wide containers prefer wide cover art before square art', () {
|
||||
final movie = _movie(artPath: '/art', backgroundSquarePath: '/square');
|
||||
|
||||
expect(movie.heroArtCandidates(containerAspectRatio: 16 / 9), ['/art', '/square']);
|
||||
expect(movie.heroArt(containerAspectRatio: 16 / 9), '/art');
|
||||
});
|
||||
|
||||
test('episodes prefer show art before episode art for wide hero containers', () {
|
||||
@@ -126,7 +123,6 @@ void main() {
|
||||
);
|
||||
|
||||
expect(episode.heroArtCandidates(containerAspectRatio: 16 / 9), ['/show-art', '/episode-art', '/square']);
|
||||
expect(episode.heroArt(containerAspectRatio: 16 / 9), '/show-art');
|
||||
expect(episode.heroArtCandidates(containerAspectRatio: 1.0), ['/square', '/show-art', '/episode-art']);
|
||||
});
|
||||
|
||||
|
||||
@@ -126,16 +126,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('MediaPlaylist.isEditable', () {
|
||||
test('smart playlists are read-only (Plex semantics)', () {
|
||||
expect(_playlist(smart: true).isEditable, isFalse);
|
||||
});
|
||||
|
||||
test('manual playlists are editable', () {
|
||||
expect(_playlist(smart: false).isEditable, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('MediaPlaylist.globalKey', () {
|
||||
test('uses "<serverId>:<id>" when serverId is set', () {
|
||||
final pl = _playlist(id: 'pl-42', serverId: 'srv-9');
|
||||
@@ -166,7 +156,6 @@ void main() {
|
||||
expect(minimal.serverName, isNull);
|
||||
expect(minimal.displayImagePath, isNull);
|
||||
expect(minimal.displayTitle, 'Min');
|
||||
expect(minimal.isEditable, isTrue);
|
||||
// Without a serverId, globalKey reduces to the bare id.
|
||||
expect(minimal.globalKey, 'pl');
|
||||
});
|
||||
|
||||
@@ -70,23 +70,5 @@ void main() {
|
||||
expect(profile.watchedIndicator, 2);
|
||||
expect(profile.defaultSubtitleForced, 1);
|
||||
});
|
||||
|
||||
test('defaults() matches parsing an empty map', () {
|
||||
final parsed = PlexUserProfile.fromJson(const {});
|
||||
final defaults = PlexUserProfile.defaults();
|
||||
|
||||
expect(defaults.autoSelectAudio, parsed.autoSelectAudio);
|
||||
expect(defaults.defaultAudioAccessibility, parsed.defaultAudioAccessibility);
|
||||
expect(defaults.defaultAudioLanguage, parsed.defaultAudioLanguage);
|
||||
expect(defaults.defaultAudioLanguages, parsed.defaultAudioLanguages);
|
||||
expect(defaults.defaultSubtitleLanguage, parsed.defaultSubtitleLanguage);
|
||||
expect(defaults.defaultSubtitleLanguages, parsed.defaultSubtitleLanguages);
|
||||
expect(defaults.autoSelectSubtitle, parsed.autoSelectSubtitle);
|
||||
expect(defaults.defaultSubtitleAccessibility, parsed.defaultSubtitleAccessibility);
|
||||
expect(defaults.defaultSubtitleForced, parsed.defaultSubtitleForced);
|
||||
expect(defaults.watchedIndicator, parsed.watchedIndicator);
|
||||
expect(defaults.mediaReviewsVisibility, parsed.mediaReviewsVisibility);
|
||||
expect(defaults.mediaReviewsLanguages, parsed.mediaReviewsLanguages);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,34 +49,17 @@ Map<String, dynamic> driftedSwitchJson() => {
|
||||
};
|
||||
|
||||
void main() {
|
||||
group('UserSwitchResponse.fromJson', () {
|
||||
test('parses a realistic drifted 201 body, preserving the token', () {
|
||||
final response = UserSwitchResponse.fromJson(driftedSwitchJson());
|
||||
|
||||
expect(response.authToken, 'minted-user-token');
|
||||
expect(response.uuid, 'e443d57860076fc3');
|
||||
expect(response.protected, isTrue);
|
||||
expect(response.homeAdmin, isTrue);
|
||||
expect(response.profile.defaultAudioLanguages, ['en', 'sv']);
|
||||
expect(response.profile.defaultSubtitleLanguages, ['en', 'sv']);
|
||||
group('parsePlexSwitchAuthToken', () {
|
||||
test('takes the token out of a realistic drifted 201 body', () {
|
||||
expect(parsePlexSwitchAuthToken(driftedSwitchJson()), 'minted-user-token');
|
||||
});
|
||||
|
||||
test('parses a token-only body with defaults everywhere else', () {
|
||||
final response = UserSwitchResponse.fromJson({'authToken': 'tok'});
|
||||
|
||||
expect(response.authToken, 'tok');
|
||||
expect(response.id, 0);
|
||||
expect(response.uuid, '');
|
||||
expect(response.title, '');
|
||||
expect(response.confirmed, isFalse);
|
||||
expect(response.homeSize, 1);
|
||||
expect(response.maxHomeSize, 1);
|
||||
expect(response.profile.autoSelectAudio, isTrue);
|
||||
expect(response.profile.defaultAudioLanguages, isNull);
|
||||
test('takes the token out of a token-only body', () {
|
||||
expect(parsePlexSwitchAuthToken({'authToken': 'tok'}), 'tok');
|
||||
});
|
||||
|
||||
test('never loses the token to wrong-typed decorative fields', () {
|
||||
final response = UserSwitchResponse.fromJson({
|
||||
final token = parsePlexSwitchAuthToken({
|
||||
'authToken': 'tok',
|
||||
'id': {},
|
||||
'uuid': 42,
|
||||
@@ -92,20 +75,13 @@ void main() {
|
||||
'twoFactorEnabled': {},
|
||||
});
|
||||
|
||||
expect(response.authToken, 'tok');
|
||||
expect(response.id, 0);
|
||||
expect(response.uuid, '42');
|
||||
expect(response.title, '7');
|
||||
expect(response.confirmed, isFalse);
|
||||
expect(response.homeSize, 1);
|
||||
expect(response.profile.autoSelectAudio, isTrue);
|
||||
expect(response.profile.defaultAudioLanguages, isNull);
|
||||
expect(token, 'tok');
|
||||
});
|
||||
|
||||
test('throws when authToken is missing, empty, or not a string', () {
|
||||
expect(() => UserSwitchResponse.fromJson(const {}), throwsFormatException);
|
||||
expect(() => UserSwitchResponse.fromJson({'authToken': ''}), throwsFormatException);
|
||||
expect(() => UserSwitchResponse.fromJson({'authToken': 12345}), throwsFormatException);
|
||||
expect(() => parsePlexSwitchAuthToken(const {}), throwsFormatException);
|
||||
expect(() => parsePlexSwitchAuthToken({'authToken': ''}), throwsFormatException);
|
||||
expect(() => parsePlexSwitchAuthToken({'authToken': 12345}), throwsFormatException);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,27 +123,16 @@ void main() {
|
||||
expect(m.serverIds, isEmpty);
|
||||
expect(m.onlineServerIds, isEmpty);
|
||||
expect(m.offlineServerIds, isEmpty);
|
||||
expect(m.plexServers, isEmpty);
|
||||
expect(m.onlineClients, isEmpty);
|
||||
});
|
||||
|
||||
test('getClient/getPlexServer return null for unknown ids', () {
|
||||
test('getClient returns null for unknown ids', () {
|
||||
final m = MultiServerManager();
|
||||
addTearDown(m.dispose);
|
||||
|
||||
expect(m.getClient(ServerId('nope')), isNull);
|
||||
expect(m.getPlexServer(ServerId('nope')), isNull);
|
||||
expect(m.isServerOnline(ServerId('nope')), isFalse);
|
||||
});
|
||||
|
||||
test('plexServers map is unmodifiable', () {
|
||||
final m = MultiServerManager();
|
||||
addTearDown(m.dispose);
|
||||
|
||||
// Map.unmodifiable rejects every mutating operation — clear() is the
|
||||
// simplest no-arg one to exercise the wrapper.
|
||||
expect(() => m.plexServers.clear(), throwsUnsupportedError);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -113,15 +113,15 @@ void main() {
|
||||
final tracks = [_sourceSubtitle(0), _sourceSubtitle(2)];
|
||||
|
||||
expect(
|
||||
PlaybackSubtitleResolver.nextSourceChoice(tracks, const PlaybackSourceSubtitleChoice.off()),
|
||||
PlaybackSubtitleResolver.advanceSourceChoice(tracks, const PlaybackSourceSubtitleChoice.off(), 1),
|
||||
const PlaybackSourceSubtitleChoice.source(0),
|
||||
);
|
||||
expect(
|
||||
PlaybackSubtitleResolver.nextSourceChoice(tracks, const PlaybackSourceSubtitleChoice.source(0)),
|
||||
PlaybackSubtitleResolver.advanceSourceChoice(tracks, const PlaybackSourceSubtitleChoice.source(0), 1),
|
||||
const PlaybackSourceSubtitleChoice.source(2),
|
||||
);
|
||||
expect(
|
||||
PlaybackSubtitleResolver.nextSourceChoice(tracks, const PlaybackSourceSubtitleChoice.source(2)),
|
||||
PlaybackSubtitleResolver.advanceSourceChoice(tracks, const PlaybackSourceSubtitleChoice.source(2), 1),
|
||||
const PlaybackSourceSubtitleChoice.off(),
|
||||
);
|
||||
expect(
|
||||
|
||||
@@ -73,11 +73,9 @@ void main() {
|
||||
addTearDown(client.close);
|
||||
final auth = PlexAuthService.forTesting(http: client);
|
||||
|
||||
final response = await auth.switchToUser('uuid-1', 'account-token');
|
||||
final token = await auth.switchToUser('uuid-1', 'account-token');
|
||||
|
||||
expect(response.authToken, 'minted-user-token');
|
||||
expect(response.profile.defaultAudioLanguages, ['en', 'sv']);
|
||||
expect(response.profile.defaultSubtitleLanguages, ['en', 'sv']);
|
||||
expect(token, 'minted-user-token');
|
||||
});
|
||||
|
||||
test('fetchServers tolerates scalar drift in server and connection fields', () async {
|
||||
|
||||
@@ -13,7 +13,7 @@ void main() {
|
||||
expect(manager.setZoomScale(1.234), 1.23);
|
||||
expect(manager.zoomScale, 1.23);
|
||||
|
||||
expect(manager.adjustZoom(VideoFilterManager.zoomStep), 1.24);
|
||||
expect(manager.setZoomScale(manager.zoomScale + VideoFilterManager.zoomStep), 1.24);
|
||||
expect(manager.zoomScale, 1.24);
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ void main() {
|
||||
|
||||
expect(manager.setZoomScale(1.00008), 1.0);
|
||||
expect(manager.zoomScale, 1.0);
|
||||
expect(manager.resetZoom(), 1.0);
|
||||
expect(manager.setZoomScale(1.0), 1.0);
|
||||
});
|
||||
|
||||
test('video zoom property is exact zero at normalized default', () async {
|
||||
|
||||
@@ -95,10 +95,6 @@ void main() {
|
||||
expect(ContentTypeHelper.isVideoContent('artist'), isFalse);
|
||||
});
|
||||
|
||||
test('isMusicLibrary returns false for null and non-matching types', () {
|
||||
expect(ContentTypeHelper.isMusicLibrary(null), isFalse);
|
||||
});
|
||||
|
||||
test('getLibraryIcon normalizes type and falls back to folder', () {
|
||||
expect(ContentTypeHelper.getLibraryIcon('MOVIE'), Symbols.movie_rounded);
|
||||
expect(ContentTypeHelper.getLibraryIcon('show'), Symbols.tv_rounded);
|
||||
|
||||
@@ -18,13 +18,6 @@ void main() {
|
||||
expect(ScreenBreakpoints.isTablet(1200), isFalse);
|
||||
});
|
||||
|
||||
test('isWideTablet: 900 ≤ w < 1200', () {
|
||||
expect(ScreenBreakpoints.isWideTablet(899.9), isFalse);
|
||||
expect(ScreenBreakpoints.isWideTablet(900), isTrue);
|
||||
expect(ScreenBreakpoints.isWideTablet(1199.9), isTrue);
|
||||
expect(ScreenBreakpoints.isWideTablet(1200), isFalse);
|
||||
});
|
||||
|
||||
test('isDesktop: 1200 ≤ w < 1600', () {
|
||||
expect(ScreenBreakpoints.isDesktop(1199.9), isFalse);
|
||||
expect(ScreenBreakpoints.isDesktop(1200), isTrue);
|
||||
@@ -32,12 +25,6 @@ void main() {
|
||||
expect(ScreenBreakpoints.isDesktop(1600), isFalse);
|
||||
});
|
||||
|
||||
test('isLargeDesktop: w ≥ 1600', () {
|
||||
expect(ScreenBreakpoints.isLargeDesktop(1599.9), isFalse);
|
||||
expect(ScreenBreakpoints.isLargeDesktop(1600), isTrue);
|
||||
expect(ScreenBreakpoints.isLargeDesktop(10000), isTrue);
|
||||
});
|
||||
|
||||
test('isDesktopOrLarger: w ≥ 1200', () {
|
||||
expect(ScreenBreakpoints.isDesktopOrLarger(1199.9), isFalse);
|
||||
expect(ScreenBreakpoints.isDesktopOrLarger(1200), isTrue);
|
||||
|
||||
@@ -61,47 +61,4 @@ void main() {
|
||||
expect(result, equals(first));
|
||||
});
|
||||
});
|
||||
|
||||
group('PlexCacheParser.extractChapters', () {
|
||||
test('returns null for null input', () {
|
||||
expect(PlexCacheParser.extractChapters(null), isNull);
|
||||
});
|
||||
|
||||
test('returns null when no metadata', () {
|
||||
expect(
|
||||
PlexCacheParser.extractChapters({
|
||||
'MediaContainer': {'Metadata': []},
|
||||
}),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null when first metadata has no Chapter key', () {
|
||||
expect(
|
||||
PlexCacheParser.extractChapters({
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
<String, dynamic>{'ratingKey': '1'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns chapter list when present', () {
|
||||
final chapters = [
|
||||
{'tag': 'Chapter 1'},
|
||||
{'tag': 'Chapter 2'},
|
||||
];
|
||||
final result = PlexCacheParser.extractChapters({
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{'ratingKey': '1', 'Chapter': chapters},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result, equals(chapters));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user