fix(jellyfin): a tap on someone a server shows names them, the password asked

Jellyfin v10.11 and later no longer say which accounts have a password
(HasPassword is always true, jellyfin/jellyfin#14950), so the demo's
account was listed as "With a password" and only named in the form.
Every listed account is now named in the form with the password field
focused, which says to leave it empty for an account without one, as
Jellyfin's own apps do; the line under each account goes.

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-14 01:52:42 +02:00
commit 107160cac4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
12 changed files with 62 additions and 66 deletions

File diff suppressed because one or more lines are too long

View file

@ -1750,9 +1750,8 @@
"jellyfinFind": "Weiter",
"jellyfinUser": "Benutzername",
"jellyfinPublicUsers": "Auf diesem Server",
"jellyfinWithoutPassword": "Ohne Passwort: ein Tippen meldet an",
"jellyfinWithPassword": "Mit Passwort",
"jellyfinPassword": "Passwort",
"jellyfinPasswordNone": "Leer lassen, wenn das Konto keins hat",
"jellyfinSignIn": "Anmelden",
"jellyfinQuickConnect": "Mit einem Code anmelden",
"jellyfinQuickConnectCode": "Gib diesen Code unter Quick Connect in einer Jellyfin-App ein, in der du angemeldet bist:",

View file

@ -2062,9 +2062,8 @@
"jellyfinFind": "Continue",
"jellyfinUser": "User name",
"jellyfinPublicUsers": "On this server",
"jellyfinWithoutPassword": "No password: one tap signs in",
"jellyfinWithPassword": "With a password",
"jellyfinPassword": "Password",
"jellyfinPasswordNone": "Leave empty if the account has none",
"jellyfinSignIn": "Sign in",
"jellyfinQuickConnect": "Sign in with a code",
"jellyfinQuickConnectCode": "Enter this code under Quick Connect in a Jellyfin app you're signed in to:",

View file

@ -10341,24 +10341,18 @@ abstract class AppLocalizations {
/// **'On this server'**
String get jellyfinPublicUsers;
/// No description provided for @jellyfinWithoutPassword.
///
/// In en, this message translates to:
/// **'No password: one tap signs in'**
String get jellyfinWithoutPassword;
/// No description provided for @jellyfinWithPassword.
///
/// In en, this message translates to:
/// **'With a password'**
String get jellyfinWithPassword;
/// No description provided for @jellyfinPassword.
///
/// In en, this message translates to:
/// **'Password'**
String get jellyfinPassword;
/// No description provided for @jellyfinPasswordNone.
///
/// In en, this message translates to:
/// **'Leave empty if the account has none'**
String get jellyfinPasswordNone;
/// No description provided for @jellyfinSignIn.
///
/// In en, this message translates to:

View file

@ -6521,15 +6521,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get jellyfinPublicUsers => 'Auf diesem Server';
@override
String get jellyfinWithoutPassword => 'Ohne Passwort: ein Tippen meldet an';
@override
String get jellyfinWithPassword => 'Mit Passwort';
@override
String get jellyfinPassword => 'Passwort';
@override
String get jellyfinPasswordNone => 'Leer lassen, wenn das Konto keins hat';
@override
String get jellyfinSignIn => 'Anmelden';

View file

@ -6465,15 +6465,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get jellyfinPublicUsers => 'On this server';
@override
String get jellyfinWithoutPassword => 'No password: one tap signs in';
@override
String get jellyfinWithPassword => 'With a password';
@override
String get jellyfinPassword => 'Password';
@override
String get jellyfinPasswordNone => 'Leave empty if the account has none';
@override
String get jellyfinSignIn => 'Sign in';

View file

@ -242,11 +242,7 @@ class HttpJellyfinGateway(
final id?,
final name?,
))
JellyfinPublicUser(
id: id,
name: name,
hasPassword: json[_key(json, 'HasPassword')] != false,
),
JellyfinPublicUser(id: id, name: name),
];
@override

View file

@ -12,15 +12,16 @@ final class const JellyfinServer({
List<Object?> get props => [address, id, name, version];
}
/// Someone a Jellyfin server shows on its login screen, to sign in as
/// with a tap: a demo's account has no password (decided 2026-10-13).
/// Someone a Jellyfin server shows on its login screen, to sign in as.
/// Whether they have a password the server doesn't say: since v10.11
/// Jellyfin keeps it to itself, so nobody learns which accounts are open
/// (decided 2026-10-14).
final class const JellyfinPublicUser({
required final String id,
required final String name,
required final bool hasPassword,
}) with Equatable {
@override
List<Object?> get props => [id, name, hasPassword];
List<Object?> get props => [id, name];
}
/// Signed in to a Jellyfin server: as whom, and the token every request

View file

@ -28,8 +28,10 @@ String jellyfinProblemText(AppLocalizations l10n, JellyfinProblem problem) =>
///
/// A server the instance suggests is filled in and found at once, and the
/// people a server shows on its login screen are listed (decided
/// 2026-10-13): one without a password, as a demo's, signs in with a tap,
/// one with a password is named in the form.
/// 2026-10-13): a tap names one in the form, which asks for the password,
/// left empty for an account without one, as a demo's. Jellyfin's own
/// apps do the same since the server stopped saying which accounts have
/// one (v10.11; decided 2026-10-14).
class const JellyfinConnectForm({super.key}) extends StatefulWidget {
@override
State<JellyfinConnectForm> createState() => _JellyfinConnectFormState();
@ -114,16 +116,12 @@ class _JellyfinConnectFormState() extends State<JellyfinConnectForm> {
).jellyfin.signIn(_server!, _user.text.trim(), _password.text),
);
/// One of the people the server shows: signed in at once without a
/// password, else named in the form, which asks for the password.
/// One of the people the server shows, named in the form, which asks
/// for the password.
void _choose(JellyfinPublicUser user) {
_user.text = user.name;
_password.clear();
if (user.hasPassword) {
_passwordFocus.requestFocus();
} else {
unawaited(_signIn());
}
}
Future<void> _startQuickConnect() => _run(() async {
@ -269,11 +267,6 @@ class _JellyfinConnectFormState() extends State<JellyfinConnectForm> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
user.hasPassword
? l10n.jellyfinWithPassword
: l10n.jellyfinWithoutPassword,
),
enabled: !_busy,
onTap: () => _choose(user),
),
@ -302,6 +295,7 @@ class _JellyfinConnectFormState() extends State<JellyfinConnectForm> {
textInputAction: TextInputAction.go,
decoration: InputDecoration(
labelText: l10n.jellyfinPassword,
helperText: l10n.jellyfinPasswordNone,
border: const OutlineInputBorder(),
),
onSubmitted: _busy ? null : (_) => _signIn(),

View file

@ -238,15 +238,19 @@ void main() {
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),
];
jellyfin
..shownUsers = const [
JellyfinPublicUser(id: 'user-2', name: 'Demo'),
JellyfinPublicUser(id: 'user-1', name: 'mika'),
]
..withoutPassword = const {'Demo'};
});
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 {
'without a password signed in with the password left empty', (
tester,
) async {
await show(
tester,
RoutePaths.studyset('japanese'),
@ -262,13 +266,16 @@ void main() {
expect(find.text('Demo'), findsOneWidget);
await tester.tap(find.text('Demo'));
await tester.pumpAndSettle();
expect(store.kept?.account, isNull);
await tester.tap(find.text('Sign in'));
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 {
testWidgets('a tap names one in the form, which asks for the '
'password', (tester) async {
await show(
tester,
RoutePaths.jellyfin('japanese'),
@ -285,6 +292,13 @@ void main() {
.text,
'mika',
);
expect(
tester
.widget<TextField>(find.widgetWithText(TextField, 'Password'))
.focusNode!
.hasFocus,
isTrue,
);
expect(store.kept?.account, isNull);
});
});

View file

@ -39,8 +39,9 @@ MockClient jellyfin12(List<http.Request> requests) => MockClient((
'id': 'e39b17f2',
'startupWizardCompleted': true,
}),
// The people the server shows on its login screen, one without a
// password (a demo's), in either case of field names.
// The people the server shows on its login screen, in either case of
// field names; whether one has a password, which servers before v10.11
// said, counts for nothing.
('GET', '/Users/Public') => json([
{'Name': 'Demo', 'Id': 'u-2', 'HasPassword': false},
{'name': 'mika', 'id': 'u-1', 'hasPassword': true},
@ -236,15 +237,14 @@ void main() {
},
);
test('lists the people a server shows on its login screen, and which '
'need a password', () async {
test('lists the people a server shows on its login screen', () async {
final server = await gateway.server(
Uri.parse('http://jellyfin.local:8096'),
);
expect(await gateway.publicUsers(server), const [
JellyfinPublicUser(id: 'u-2', name: 'Demo', hasPassword: false),
JellyfinPublicUser(id: 'u-1', name: 'mika', hasPassword: true),
JellyfinPublicUser(id: 'u-2', name: 'Demo'),
JellyfinPublicUser(id: 'u-1', name: 'mika'),
]);
});

View file

@ -32,10 +32,13 @@ class FakeJellyfinGateway({
/// Whether the Quick Connect code was approved.
bool approved = false;
/// The people the server shows on its login screen; those without a
/// password sign in with none.
/// The people the server shows on its login screen; those named in
/// [withoutPassword] sign in with none.
List<JellyfinPublicUser> shownUsers = const [];
/// Names of accounts without a password, as a demo's.
Set<String> withoutPassword = const {};
/// What was reported of playbacks: their positions, in order.
final reports = <({Duration position, bool started, bool stopped})>[];
@ -90,7 +93,9 @@ class FakeJellyfinGateway({
String password,
) async {
for (final public in shownUsers) {
if (public.name == user && !public.hasPassword && password.isEmpty) {
if (public.name == user &&
withoutPassword.contains(user) &&
password.isEmpty) {
return _account('token-3', userId: public.id, userName: public.name);
}
}