From 9c1a5be6dec9f8592e602f29b5c481d760fbb59c Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:36:19 +0100 Subject: [PATCH 01/15] feat: add Seerr QuickConnect API endpoints to chopper service --- lib/seerr/seerr_chopper_service.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/seerr/seerr_chopper_service.dart b/lib/seerr/seerr_chopper_service.dart index 0f489476e..a7ac1c901 100644 --- a/lib/seerr/seerr_chopper_service.dart +++ b/lib/seerr/seerr_chopper_service.dart @@ -28,6 +28,21 @@ abstract class SeerrChopperService extends ChopperService { Map? headers, }); + @POST(path: '/auth/jellyfin/quickconnect/initiate') + Future> quickConnectInitiate(); + + // Security note: secret is passed as a query parameter per the Seerr API spec. + // Ensure the Seerr server URL uses HTTPS in production to protect this value in transit. + @GET(path: '/auth/jellyfin/quickconnect/check') + Future> quickConnectCheck( + @Query('secret') String secret, + ); + + @POST(path: '/auth/jellyfin/quickconnect/authenticate') + Future> quickConnectAuthenticate( + @Body() SeerrQuickConnectAuthBody body, + ); + @POST(path: '/auth/logout') Future> logout(); From 10044dccfc46cedacb9fe0d028be729e7ebceaca Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:37:03 +0100 Subject: [PATCH 02/15] feat: add Seerr QuickConnect model classes --- lib/seerr/seerr_models.dart | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/seerr/seerr_models.dart b/lib/seerr/seerr_models.dart index 1a36286d9..2b5933d6e 100644 --- a/lib/seerr/seerr_models.dart +++ b/lib/seerr/seerr_models.dart @@ -1418,6 +1418,46 @@ class SeerrAuthJellyfinBody { Map toJson() => _$SeerrAuthJellyfinBodyToJson(this); } +@JsonSerializable() +class SeerrQuickConnectInitResponse { + final String code; + final String secret; + + SeerrQuickConnectInitResponse({ + required this.code, + required this.secret, + }); + + factory SeerrQuickConnectInitResponse.fromJson(Map json) => + _$SeerrQuickConnectInitResponseFromJson(json); + Map toJson() => _$SeerrQuickConnectInitResponseToJson(this); +} + +@JsonSerializable() +class SeerrQuickConnectCheckResponse { + final bool authenticated; + + SeerrQuickConnectCheckResponse({ + required this.authenticated, + }); + + factory SeerrQuickConnectCheckResponse.fromJson(Map json) => + _$SeerrQuickConnectCheckResponseFromJson(json); + Map toJson() => _$SeerrQuickConnectCheckResponseToJson(this); +} + +@JsonSerializable() +class SeerrQuickConnectAuthBody { + final String secret; + + SeerrQuickConnectAuthBody({ + required this.secret, + }); + + factory SeerrQuickConnectAuthBody.fromJson(Map json) => _$SeerrQuickConnectAuthBodyFromJson(json); + Map toJson() => _$SeerrQuickConnectAuthBodyToJson(this); +} + class SeerrCompany { final int id; final String name; From c23d009170a3a00b9db319eb4488c0b8cc11a902 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:37:38 +0100 Subject: [PATCH 03/15] feat: register Seerr QuickConnect types in JSON converter --- lib/seerr/seerr_json_converter.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/seerr/seerr_json_converter.dart b/lib/seerr/seerr_json_converter.dart index 945e15cb5..e1ba0b714 100644 --- a/lib/seerr/seerr_json_converter.dart +++ b/lib/seerr/seerr_json_converter.dart @@ -40,6 +40,9 @@ class SeerrJsonConverter extends JsonConverter { SeerrUsersResponse: SeerrUsersResponse.fromJson, SeerrAuthLocalBody: SeerrAuthLocalBody.fromJson, SeerrAuthJellyfinBody: SeerrAuthJellyfinBody.fromJson, + SeerrQuickConnectInitResponse: SeerrQuickConnectInitResponse.fromJson, + SeerrQuickConnectCheckResponse: SeerrQuickConnectCheckResponse.fromJson, + SeerrQuickConnectAuthBody: SeerrQuickConnectAuthBody.fromJson, SeerrGenreResponse: SeerrGenreResponse.fromJson, SeerrWatchProvider: SeerrWatchProvider.fromJson, SeerrWatchProviderRegion: SeerrWatchProviderRegion.fromJson, From c1dbaf4b56cde4671c3ee8622bca262b023ed4d0 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:38:38 +0100 Subject: [PATCH 04/15] feat: add Seerr QuickConnect service methods --- lib/providers/seerr_service_provider.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/providers/seerr_service_provider.dart b/lib/providers/seerr_service_provider.dart index 292c7ee2e..0e50a7780 100644 --- a/lib/providers/seerr_service_provider.dart +++ b/lib/providers/seerr_service_provider.dart @@ -656,6 +656,25 @@ class SeerrService { return _requireSessionCookie(response, label: 'Jellyfin'); } + Future quickConnectInitiate() async { + final response = await _api.quickConnectInitiate(); + if (!response.isSuccessful) return null; + return response.body; + } + + Future quickConnectCheck(String secret) async { + final response = await _api.quickConnectCheck(secret); + return response.body?.authenticated ?? false; + } + + Future quickConnectAuthenticate(String secret) async { + final response = await _api.quickConnectAuthenticate( + SeerrQuickConnectAuthBody(secret: secret), + ); + if (!response.isSuccessful) return null; + return _extractSessionCookie(response); + } + Future logout() async => await _api.logout(); Future> _authenticateJellyfin( From e2dfbb06de89a12cb394734a33b0358ec5d779aa Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:40:30 +0100 Subject: [PATCH 05/15] feat: add Seerr QuickConnect tab to connection dialog --- .../widgets/seerr_connection_dialog.dart | 202 ++++++++++++++++-- 1 file changed, 185 insertions(+), 17 deletions(-) diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 52808eee2..245a885f1 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -16,6 +19,7 @@ import 'package:fladder/screens/shared/fladder_notification_overlay.dart'; import 'package:fladder/screens/shared/focused_outlined_text_field.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/seerr/seerr_models.dart'; +import 'package:fladder/util/clipboard_helper.dart'; import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -36,12 +40,14 @@ Future showSeerrConnectionDialog(BuildContext context) { enum SeerrAuthTab { jellyfin, local, - apiKey; + apiKey, + quickConnect; String label(BuildContext context) => switch (this) { SeerrAuthTab.apiKey => context.localized.seerrAuthApiKey, SeerrAuthTab.local => context.localized.seerrAuthLocal, SeerrAuthTab.jellyfin => context.localized.seerrAuthJellyfin, + SeerrAuthTab.quickConnect => context.localized.seerrAuthQuickConnect, }; } @@ -69,6 +75,13 @@ class _SeerrConnectionDialogState extends ConsumerState { String? error; String? warning; + // QuickConnect state + static const _maxPollAttempts = 150; // ~5 minutes at 2s interval + String? _qcCode; + String? _qcSecret; + RestartableTimer? _qcTimer; + int _qcPollAttempts = 0; + bool get _hasPresetSeerrBaseUrl => FladderConfig.seerrBaseUrl?.isNotEmpty == true; @override @@ -89,6 +102,9 @@ class _SeerrConnectionDialogState extends ConsumerState { @override void dispose() { + _qcTimer?.cancel(); + _qcSecret = null; + _qcCode = null; apiKeyController.dispose(); serverController.dispose(); localEmailController.dispose(); @@ -298,6 +314,109 @@ class _SeerrConnectionDialogState extends ConsumerState { } } + Future _quickConnectInitiate() async { + if (_disableAuthActions) return; + if (!_applyServerUrl()) return; + setState(() { + processing = true; + error = null; + _qcCode = null; + _qcSecret = null; + }); + _qcTimer?.cancel(); + + try { + final result = await ref.read(seerrApiProvider).quickConnectInitiate(); + if (!mounted) return; + if (result == null) { + setState(() { + error = context.localized.quickConnectPostFailed; + processing = false; + }); + return; + } + setState(() { + _qcCode = result.code; + _qcSecret = result.secret; + processing = false; + }); + _startQcPolling(); + } catch (e) { + if (mounted) { + setState(() { + error = e.toString(); + processing = false; + }); + } + } + } + + void _startQcPolling() { + _qcTimer?.cancel(); + _qcPollAttempts = 0; + final secret = _qcSecret; + if (secret == null) return; + _qcTimer = RestartableTimer(const Duration(seconds: 2), () async { + if (_qcPollAttempts >= _maxPollAttempts) { + _qcTimer?.cancel(); + return; + } + _qcPollAttempts++; + try { + final authenticated = await ref.read(seerrApiProvider).quickConnectCheck(secret); + if (!mounted) return; + if (authenticated) { + await _quickConnectAuthenticate(secret); + } else { + _qcTimer?.reset(); + } + } catch (_) { + if (mounted) _qcTimer?.reset(); + } + }); + } + + Future _quickConnectAuthenticate(String secret) async { + _qcTimer?.cancel(); + setState(() { + processing = true; + error = null; + }); + + try { + final cookie = await ref.read(seerrApiProvider).quickConnectAuthenticate(secret); + if (!mounted) return; + if (cookie == null || cookie.isEmpty) { + setState(() { + error = context.localized.seerrUserFetchFailed; + processing = false; + }); + return; + } + ref.read(userProvider.notifier).setSeerrSessionCookie(cookie); + ref.read(userProvider.notifier).setSeerrApiKey(''); + await _refreshSession(); + if (mounted) { + FladderSnack.show(context.localized.seerrLoggedIn, context: context); + } + } catch (e) { + if (mounted) { + setState(() { + error = e.toString(); + }); + } + } finally { + if (mounted) { + setState(() { + processing = false; + _qcCode = null; + _qcSecret = null; + }); + ref.read(seerrDashboardProvider.notifier).clear(); + } + } + } + Future _logout() async { final serverUrl = serverController.text.trim(); if (serverUrl.isNotEmpty) { @@ -494,22 +613,25 @@ class _SeerrConnectionDialogState extends ConsumerState { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: SegmentedButton( - segments: SeerrAuthTab.values - .map( - (tab) => ButtonSegment( - value: tab, - label: Text(tab.label(context)), - ), - ) - .toList(), - selected: {selectedTab}, - onSelectionChanged: (value) { - setState(() { - selectedTab = value.first; - }); - }, - showSelectedIcon: false, + child: SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: SeerrAuthTab.values + .map( + (tab) => ButtonSegment( + value: tab, + label: Text(tab.label(context)), + ), + ) + .toList(), + selected: {selectedTab}, + onSelectionChanged: (value) { + setState(() { + selectedTab = value.first; + }); + }, + showSelectedIcon: false, + ), ), ), AnimatedFadeSize(child: _authForm()), @@ -620,6 +742,52 @@ class _SeerrConnectionDialogState extends ConsumerState { ), ], ); + case SeerrAuthTab.quickConnect: + return Column( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + if (_qcCode != null) ...[ + Text( + context.localized.quickConnectEnterCodeDescription, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + GestureDetector( + onTap: () => context.copyToClipboard(_qcCode!), + child: IntrinsicWidth( + child: Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + _qcCode!, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + wordSpacing: 8, + letterSpacing: 8, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + ], + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded( + child: FilledButton( + onPressed: processing ? null : _quickConnectInitiate, + child: processing + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) + : Text(_qcCode != null ? context.localized.refresh : context.localized.quickConnectTitle), + ), + ), + ], + ), + ], + ); } } From 5fb9b1e448999c7a4b02740cb641834d7a10c5ca Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:41:08 +0100 Subject: [PATCH 06/15] feat: add polling timeout and error handling to Jellyfin QuickConnect dialog --- lib/screens/login/login_code_dialog.dart | 26 +++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/screens/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index 61121ce01..676b88da2 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -40,7 +40,9 @@ class LoginCodeDialog extends ConsumerStatefulWidget { class _LoginCodeDialogState extends ConsumerState { late QuickConnectResult quickConnectInfo = widget.quickConnectInfo; + static const _maxPollAttempts = 300; // ~5 minutes at 1s interval RestartableTimer? timer; + int _pollAttempts = 0; @override void initState() { @@ -56,14 +58,24 @@ class _LoginCodeDialogState extends ConsumerState { void createTimer() { timer?.cancel(); + _pollAttempts = 0; timer = RestartableTimer(const Duration(seconds: 1), () async { - final result = await ref.read(jellyApiProvider).quickConnectConnectGet( - secret: quickConnectInfo.secret, - ); - final newSecret = result.body?.secret; - if (result.isSuccessful && result.body?.authenticated == true && newSecret != null) { - widget.onAuthenticated.call(context, newSecret); - } else { + if (_pollAttempts >= _maxPollAttempts) { + timer?.cancel(); + return; + } + _pollAttempts++; + try { + final result = await ref.read(jellyApiProvider).quickConnectConnectGet( + secret: quickConnectInfo.secret, + ); + final newSecret = result.body?.secret; + if (result.isSuccessful && result.body?.authenticated == true && newSecret != null) { + widget.onAuthenticated.call(context, newSecret); + } else { + timer?.reset(); + } + } catch (_) { timer?.reset(); } }); From cffec8188d65363a349d7ff34a1566b9f3643e61 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:43:21 +0100 Subject: [PATCH 07/15] feat: integrate Seerr QuickConnect into login flow --- .../login/login_screen_credentials.dart | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 1349a34a2..76751ecdb 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -377,26 +377,33 @@ class _LoginScreenCredentialsState extends ConsumerState Future _tryAuthenticateSeerr(String seerrUrl) async { try { - final username = usernameController.text.trim(); - final password = passwordController.text; - final effectiveSeerrUrl = FladderConfig.seerrBaseUrl ?? seerrUrl; ref.read(userProvider.notifier).setSeerrServerUrl(effectiveSeerrUrl); final tempCookie = ref.read(authProvider.select((value) => value.tempSeerrSessionCookie)); - final cookie = tempCookie ?? - await ref.read(seerrApiProvider).authenticateJellyfin( - username: username, - password: password, - ); - - ref.read(userProvider.notifier).setSeerrSessionCookie(cookie); - ref.read(userProvider.notifier).setSeerrApiKey(''); - ref.read(authProvider.notifier).setTempSeerrSessionCookie(null); + if (tempCookie != null) { + ref.read(userProvider.notifier).setSeerrSessionCookie(tempCookie); + ref.read(userProvider.notifier).setSeerrApiKey(''); + ref.read(authProvider.notifier).setTempSeerrSessionCookie(null); + if (context.mounted) { + FladderSnack.show(context.localized.seerrLoggedIn, context: context); + } + return; + } - if (context.mounted) { - FladderSnack.show(context.localized.seerrLoggedIn, context: context); + final password = passwordController.text; + if (password.isNotEmpty) { + final cookie = await ref.read(seerrApiProvider).authenticateJellyfin( + username: usernameController.text.trim(), + password: password, + ); + ref.read(userProvider.notifier).setSeerrSessionCookie(cookie); + ref.read(userProvider.notifier).setSeerrApiKey(''); + if (context.mounted) { + FladderSnack.show(context.localized.seerrLoggedIn, context: context); + } } + // No password (QC login) → skip auto-auth, user can authenticate via settings } catch (e) { if (context.mounted) { FladderSnack.show( @@ -415,7 +422,13 @@ class _LoginScreenCredentialsState extends ConsumerState ref.read(authProvider.notifier).authenticateUsingSecret(secret), ); if (response.isSuccess && context.mounted) { - loggedInGoToHome(context, ref); + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + if (tempSeerrUrl != null && tempSeerrUrl.isNotEmpty) { + await _tryAuthenticateSeerr(tempSeerrUrl); + } + if (context.mounted) { + loggedInGoToHome(context, ref); + } } setState(() { loggingIn = false; From 324dad3d7929edbc17a55bf813eab94eef77bab5 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:44:06 +0100 Subject: [PATCH 08/15] feat: add seerrAuthQuickConnect localization string --- lib/l10n/app_en.arb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2b3b7344b..9599145b0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2168,6 +2168,8 @@ "@seerrAuthLocal": {}, "seerrAuthJellyfin": "Jellyfin", "@seerrAuthJellyfin": {}, + "seerrAuthQuickConnect": "QuickConnect", + "@seerrAuthQuickConnect": {}, "seerrUserFetchFailed": "Failed to fetch user from Seerr", "@seerrUserFetchFailed": {}, "seerrEnterServerUrlFirst": "Enter a Seerr server URL first", From 0bc9e6826195f72c6c5d2802d38d921f42d6e8a2 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:47:07 +0100 Subject: [PATCH 09/15] build: regenerate chopper and json_serializable output for Seerr QuickConnect --- lib/seerr/seerr_chopper_service.chopper.dart | 42 ++++++++++++++++++++ lib/seerr/seerr_models.g.dart | 38 ++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/lib/seerr/seerr_chopper_service.chopper.dart b/lib/seerr/seerr_chopper_service.chopper.dart index 2fdb09ed5..b774b79b7 100644 --- a/lib/seerr/seerr_chopper_service.chopper.dart +++ b/lib/seerr/seerr_chopper_service.chopper.dart @@ -72,6 +72,48 @@ final class _$SeerrChopperService extends SeerrChopperService { return client.send($request); } + @override + Future> quickConnectInitiate() { + final Uri $url = Uri.parse('/api/v1/auth/jellyfin/quickconnect/initiate'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> quickConnectCheck( + String secret) { + final Uri $url = Uri.parse('/api/v1/auth/jellyfin/quickconnect/check'); + final Map $params = {'secret': secret}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> quickConnectAuthenticate( + SeerrQuickConnectAuthBody body) { + final Uri $url = + Uri.parse('/api/v1/auth/jellyfin/quickconnect/authenticate'); + final $body = body; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + @override Future> logout() { final Uri $url = Uri.parse('/api/v1/auth/logout'); diff --git a/lib/seerr/seerr_models.g.dart b/lib/seerr/seerr_models.g.dart index a788b38a3..28be27cab 100644 --- a/lib/seerr/seerr_models.g.dart +++ b/lib/seerr/seerr_models.g.dart @@ -844,6 +844,44 @@ Map _$SeerrAuthJellyfinBodyToJson( if (instance.hostname case final value?) 'hostname': value, }; +SeerrQuickConnectInitResponse _$SeerrQuickConnectInitResponseFromJson( + Map json) => + SeerrQuickConnectInitResponse( + code: json['code'] as String, + secret: json['secret'] as String, + ); + +Map _$SeerrQuickConnectInitResponseToJson( + SeerrQuickConnectInitResponse instance) => + { + 'code': instance.code, + 'secret': instance.secret, + }; + +SeerrQuickConnectCheckResponse _$SeerrQuickConnectCheckResponseFromJson( + Map json) => + SeerrQuickConnectCheckResponse( + authenticated: json['authenticated'] as bool, + ); + +Map _$SeerrQuickConnectCheckResponseToJson( + SeerrQuickConnectCheckResponse instance) => + { + 'authenticated': instance.authenticated, + }; + +SeerrQuickConnectAuthBody _$SeerrQuickConnectAuthBodyFromJson( + Map json) => + SeerrQuickConnectAuthBody( + secret: json['secret'] as String, + ); + +Map _$SeerrQuickConnectAuthBodyToJson( + SeerrQuickConnectAuthBody instance) => + { + 'secret': instance.secret, + }; + _SeerrUserModel _$SeerrUserModelFromJson(Map json) => _SeerrUserModel( id: (json['id'] as num?)?.toInt(), From 7d48552e4e7fb13ca682de9d4a012b0fc46e264f Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:04:25 +0100 Subject: [PATCH 10/15] feat: add "Open Jellyfin QuickConnect" link to Seerr QuickConnect tab --- lib/l10n/app_en.arb | 2 ++ .../settings/widgets/seerr_connection_dialog.dart | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9599145b0..1fa284482 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1546,6 +1546,8 @@ "quickConnectPostFailed": "Failed to get quick connect code", "quickConnectLoginUsingCode": "Using quick connect", "quickConnectEnterCodeDescription": "Enter the code below to login", + "openJellyfinQuickConnect": "Open Jellyfin QuickConnect", + "@openJellyfinQuickConnect": {}, "showMore": "Show more", "showLess": "Show less", "itemColorsTitle": "Item colors", diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 245a885f1..7a52c7e7c 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -8,6 +8,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/providers/api_provider.dart'; +import 'package:fladder/screens/shared/media/external_urls.dart' as ext; import 'package:fladder/providers/seerr_api_provider.dart'; import 'package:fladder/providers/seerr_dashboard_provider.dart'; import 'package:fladder/providers/seerr_user_provider.dart'; @@ -772,6 +773,17 @@ class _SeerrConnectionDialogState extends ConsumerState { ), ), ), + TextButton.icon( + onPressed: () async { + final baseUrl = FladderConfig.baseUrl ?? ref.read(userProvider)?.credentials.url; + if (baseUrl != null && baseUrl.isNotEmpty) { + await ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + _qcTimer?.reset(); + } + }, + icon: const Icon(IconsaxPlusLinear.export_1), + label: Text(context.localized.openJellyfinQuickConnect), + ), ], Row( mainAxisAlignment: MainAxisAlignment.end, From dccdead4e7202dbed009e4e5120e46f9786e1c94 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:20:12 +0100 Subject: [PATCH 11/15] remove comment --- lib/screens/login/login_screen_credentials.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 76751ecdb..a58af3eb7 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -403,7 +403,7 @@ class _LoginScreenCredentialsState extends ConsumerState FladderSnack.show(context.localized.seerrLoggedIn, context: context); } } - // No password (QC login) → skip auto-auth, user can authenticate via settings + } catch (e) { if (context.mounted) { FladderSnack.show( From 7cd0eb9e63148eded13ab8d04d39219c2fe1337e Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:42:18 +0100 Subject: [PATCH 12/15] style: fix linting issues --- .../login/login_screen_credentials.dart | 1 - lib/seerr/seerr_chopper_service.chopper.dart | 77 ++++++------------- 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index a58af3eb7..4a0463837 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -403,7 +403,6 @@ class _LoginScreenCredentialsState extends ConsumerState FladderSnack.show(context.localized.seerrLoggedIn, context: context); } } - } catch (e) { if (context.mounted) { FladderSnack.show( diff --git a/lib/seerr/seerr_chopper_service.chopper.dart b/lib/seerr/seerr_chopper_service.chopper.dart index b774b79b7..5459dfb97 100644 --- a/lib/seerr/seerr_chopper_service.chopper.dart +++ b/lib/seerr/seerr_chopper_service.chopper.dart @@ -80,13 +80,11 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override - Future> quickConnectCheck( - String secret) { + Future> quickConnectCheck(String secret) { final Uri $url = Uri.parse('/api/v1/auth/jellyfin/quickconnect/check'); final Map $params = {'secret': secret}; final Request $request = Request( @@ -95,15 +93,13 @@ final class _$SeerrChopperService extends SeerrChopperService { client.baseUrl, parameters: $params, ); - return client.send($request); + return client.send($request); } @override - Future> quickConnectAuthenticate( SeerrQuickConnectAuthBody body) { - final Uri $url = - Uri.parse('/api/v1/auth/jellyfin/quickconnect/authenticate'); + Future> quickConnectAuthenticate(SeerrQuickConnectAuthBody body) { + final Uri $url = Uri.parse('/api/v1/auth/jellyfin/quickconnect/authenticate'); final $body = body; final Request $request = Request( 'POST', @@ -144,8 +140,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client - .send($request); + return client.send($request); } @override @@ -167,8 +162,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client - .send($request); + return client.send($request); } @override @@ -198,9 +192,7 @@ final class _$SeerrChopperService extends SeerrChopperService { String? language, }) { final Uri $url = Uri.parse('/api/v1/movie/${movieId}'); - final Map $params = { - 'language': language - }; + final Map $params = {'language': language}; final Request $request = Request( 'GET', $url, @@ -216,9 +208,7 @@ final class _$SeerrChopperService extends SeerrChopperService { String? language, }) { final Uri $url = Uri.parse('/api/v1/tv/${tvId}'); - final Map $params = { - 'language': language - }; + final Map $params = {'language': language}; final Request $request = Request( 'GET', $url, @@ -235,9 +225,7 @@ final class _$SeerrChopperService extends SeerrChopperService { String? language, }) { final Uri $url = Uri.parse('/api/v1/tv/${tvId}/season/${seasonNumber}'); - final Map $params = { - 'language': language - }; + final Map $params = {'language': language}; final Request $request = Request( 'GET', $url, @@ -306,8 +294,7 @@ final class _$SeerrChopperService extends SeerrChopperService { } @override - Future> createRequest( - SeerrCreateRequestBody body) { + Future> createRequest(SeerrCreateRequestBody body) { final Uri $url = Uri.parse('/api/v1/request'); final $body = body; final Request $request = Request( @@ -573,9 +560,7 @@ final class _$SeerrChopperService extends SeerrChopperService { String? language, }) { final Uri $url = Uri.parse('/api/v1/movie/${movieId}/similar'); - final Map $params = { - 'language': language - }; + final Map $params = {'language': language}; final Request $request = Request( 'GET', $url, @@ -591,9 +576,7 @@ final class _$SeerrChopperService extends SeerrChopperService { String? language, }) { final Uri $url = Uri.parse('/api/v1/tv/${tvId}/similar'); - final Map $params = { - 'language': language - }; + final Map $params = {'language': language}; final Request $request = Request( 'GET', $url, @@ -609,9 +592,7 @@ final class _$SeerrChopperService extends SeerrChopperService { String? language, }) { final Uri $url = Uri.parse('/api/v1/movie/${movieId}/recommendations'); - final Map $params = { - 'language': language - }; + final Map $params = {'language': language}; final Request $request = Request( 'GET', $url, @@ -649,9 +630,7 @@ final class _$SeerrChopperService extends SeerrChopperService { String? language, }) { final Uri $url = Uri.parse('/api/v1/tv/${tvId}/recommendations'); - final Map $params = { - 'language': language - }; + final Map $params = {'language': language}; final Request $request = Request( 'GET', $url, @@ -698,8 +677,7 @@ final class _$SeerrChopperService extends SeerrChopperService { client.baseUrl, parameters: $params, ); - return client - .send($request); + return client.send($request); } @override @@ -725,12 +703,9 @@ final class _$SeerrChopperService extends SeerrChopperService { } @override - Future>> getMovieWatchProviders( - {String? watchRegion}) { + Future>> getMovieWatchProviders({String? watchRegion}) { final Uri $url = Uri.parse('/api/v1/watchproviders/movies'); - final Map $params = { - 'watchRegion': watchRegion - }; + final Map $params = {'watchRegion': watchRegion}; final Request $request = Request( 'GET', $url, @@ -741,12 +716,9 @@ final class _$SeerrChopperService extends SeerrChopperService { } @override - Future>> getTvWatchProviders( - {String? watchRegion}) { + Future>> getTvWatchProviders({String? watchRegion}) { final Uri $url = Uri.parse('/api/v1/watchproviders/tv'); - final Map $params = { - 'watchRegion': watchRegion - }; + final Map $params = {'watchRegion': watchRegion}; final Request $request = Request( 'GET', $url, @@ -764,8 +736,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send, - SeerrWatchProviderRegion>($request); + return client.send, SeerrWatchProviderRegion>($request); } @override @@ -776,8 +747,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override @@ -788,7 +758,6 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } } From 440259fb5dc098917d42a2f9daf2b1dd7243ee3d Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:43:14 +0100 Subject: [PATCH 13/15] style: fix linting issues --- lib/seerr/seerr_chopper_service.chopper.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/seerr/seerr_chopper_service.chopper.dart b/lib/seerr/seerr_chopper_service.chopper.dart index 5459dfb97..0c33846cb 100644 --- a/lib/seerr/seerr_chopper_service.chopper.dart +++ b/lib/seerr/seerr_chopper_service.chopper.dart @@ -97,7 +97,6 @@ final class _$SeerrChopperService extends SeerrChopperService { } @override - SeerrQuickConnectAuthBody body) { Future> quickConnectAuthenticate(SeerrQuickConnectAuthBody body) { final Uri $url = Uri.parse('/api/v1/auth/jellyfin/quickconnect/authenticate'); final $body = body; From 16cfffb491c5ef677f7b57ef851b1e608ba6a83e Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:30:35 +0100 Subject: [PATCH 14/15] fix: remove stale _disableAuthActions reference in Seerr QuickConnect --- lib/screens/settings/widgets/seerr_connection_dialog.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 7a52c7e7c..6f7ca941b 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -316,7 +316,6 @@ class _SeerrConnectionDialogState extends ConsumerState { } Future _quickConnectInitiate() async { - if (_disableAuthActions) return; if (!_applyServerUrl()) return; setState(() { processing = true; From 9132a8320001cfc4f4d2cd74afa2d1d98d788302 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:52:46 +0100 Subject: [PATCH 15/15] fix: add missing await on _applyServerUrl in QuickConnect initiate --- lib/screens/settings/widgets/seerr_connection_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 6f7ca941b..2604baaab 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -316,7 +316,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } Future _quickConnectInitiate() async { - if (!_applyServerUrl()) return; + if (!await _applyServerUrl()) return; setState(() { processing = true; error = null;