feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap

An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.

In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-13 23:25:33 +02:00
commit 85506ac7eb
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,6 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/data/jellyfin_suggestion.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/model/jellyfin.dart';
import 'package:halkyon_learn_flutter/src/feature/jellyfin/widget/watch_screen.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
@ -21,7 +23,11 @@ void main() {
store = MemoryJellyfinStore();
});
Future<void> show(WidgetTester tester, String location) async {
Future<void> show(
WidgetTester tester,
String location, {
String? suggested,
}) async {
tester.view.physicalSize = const Size(1200, 1400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
@ -44,6 +50,7 @@ void main() {
dictionaryGateway: sampleDictionary(),
jellyfinGateway: jellyfin,
jellyfinStore: store,
jellyfinSuggestion: JellyfinSuggestion(() async => suggested),
),
location: location,
);
@ -228,4 +235,57 @@ void main() {
expect(find.text('Libraries'), findsOneWidget);
});
group('a Jellyfin server the instance suggests (decided 2026-10-13)', () {
setUp(() {
jellyfin.shownUsers = const [
JellyfinPublicUser(id: 'user-2', name: 'Demo', hasPassword: false),
JellyfinPublicUser(id: 'user-1', name: 'mika', hasPassword: true),
];
});
testWidgets('Add material offers it before signing in; the sign-in '
"finds it by itself and lists the server's public people, one "
'without a password signed in with a tap', (tester) async {
await show(
tester,
RoutePaths.studyset('japanese'),
suggested: 'https://jellyfin.local:8920',
);
await tester.tap(find.text('Add material'));
await tester.pumpAndSettle();
await tester.tap(find.text('From Jellyfin'));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('Living room'), findsOneWidget);
expect(find.text('Demo'), findsOneWidget);
await tester.tap(find.text('Demo'));
await tester.pumpAndSettle();
expect(find.text('Libraries'), findsOneWidget);
expect(store.kept?.account?.userName, 'Demo');
});
testWidgets('one with a password is named in the form, which asks for '
'it', (tester) async {
await show(
tester,
RoutePaths.jellyfin('japanese'),
suggested: 'https://jellyfin.local:8920',
);
await tester.tap(find.text('mika'));
await tester.pumpAndSettle();
expect(
tester
.widget<TextField>(find.widgetWithText(TextField, 'User name'))
.controller!
.text,
'mika',
);
expect(store.kept?.account, isNull);
});
});
}