diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2b3b7344b..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", @@ -2168,6 +2170,8 @@ "@seerrAuthLocal": {}, "seerrAuthJellyfin": "Jellyfin", "@seerrAuthJellyfin": {}, + "seerrAuthQuickConnect": "QuickConnect", + "@seerrAuthQuickConnect": {}, "seerrUserFetchFailed": "Failed to fetch user from Seerr", "@seerrUserFetchFailed": {}, "seerrEnterServerUrlFirst": "Enter a Seerr server URL first", 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( 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(); } }); diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 1349a34a2..4a0463837 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -377,25 +377,31 @@ 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); + } } } catch (e) { if (context.mounted) { @@ -415,7 +421,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; diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 52808eee2..2604baaab 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'; @@ -5,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'; @@ -16,6 +20,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 +41,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 +76,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 +103,9 @@ class _SeerrConnectionDialogState extends ConsumerState { @override void dispose() { + _qcTimer?.cancel(); + _qcSecret = null; + _qcCode = null; apiKeyController.dispose(); serverController.dispose(); localEmailController.dispose(); @@ -298,6 +315,108 @@ class _SeerrConnectionDialogState extends ConsumerState { } } + Future _quickConnectInitiate() async { + if (!await _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,63 @@ 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, + ), + ), + ), + ), + ), + 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, + 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), + ), + ), + ], + ), + ], + ); } } diff --git a/lib/seerr/seerr_chopper_service.chopper.dart b/lib/seerr/seerr_chopper_service.chopper.dart index 2fdb09ed5..0c33846cb 100644 --- a/lib/seerr/seerr_chopper_service.chopper.dart +++ b/lib/seerr/seerr_chopper_service.chopper.dart @@ -72,6 +72,43 @@ 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'); @@ -102,8 +139,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client - .send($request); + return client.send($request); } @override @@ -125,8 +161,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client - .send($request); + return client.send($request); } @override @@ -156,9 +191,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, @@ -174,9 +207,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, @@ -193,9 +224,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, @@ -264,8 +293,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( @@ -531,9 +559,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, @@ -549,9 +575,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, @@ -567,9 +591,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, @@ -607,9 +629,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, @@ -656,8 +676,7 @@ final class _$SeerrChopperService extends SeerrChopperService { client.baseUrl, parameters: $params, ); - return client - .send($request); + return client.send($request); } @override @@ -683,12 +702,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, @@ -699,12 +715,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, @@ -722,8 +735,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send, - SeerrWatchProviderRegion>($request); + return client.send, SeerrWatchProviderRegion>($request); } @override @@ -734,8 +746,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override @@ -746,7 +757,6 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } } 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(); 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, 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; 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(),