From 5abd3172378d1b47a436ec1afdf7783ac69d6bb1 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:44:27 +0100 Subject: [PATCH 01/63] feat(Seerr): add QuickConnect API endpoints and models --- .../seerr/seerr_request_provider.g.dart | 2 +- lib/seerr/seerr_chopper_service.chopper.dart | 42 +++++++++++++++++++ lib/seerr/seerr_chopper_service.dart | 13 ++++++ lib/seerr/seerr_json_converter.dart | 3 ++ lib/seerr/seerr_models.dart | 41 ++++++++++++++++++ lib/seerr/seerr_models.g.dart | 38 +++++++++++++++++ pubspec.lock | 20 ++++----- 7 files changed, 148 insertions(+), 11 deletions(-) diff --git a/lib/providers/seerr/seerr_request_provider.g.dart b/lib/providers/seerr/seerr_request_provider.g.dart index 15a0d5144..b901cc25b 100644 --- a/lib/providers/seerr/seerr_request_provider.g.dart +++ b/lib/providers/seerr/seerr_request_provider.g.dart @@ -6,7 +6,7 @@ part of 'seerr_request_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$seerrRequestHash() => r'89604c3fa7ae8c3cd68218b8fe89c1df284aea5d'; +String _$seerrRequestHash() => r'5c36189f4c33f2b035b6dc23ddd53e134276b0f5'; /// See also [SeerrRequest]. @ProviderFor(SeerrRequest) 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_chopper_service.dart b/lib/seerr/seerr_chopper_service.dart index 0f489476e..6d06079d7 100644 --- a/lib/seerr/seerr_chopper_service.dart +++ b/lib/seerr/seerr_chopper_service.dart @@ -28,6 +28,19 @@ abstract class SeerrChopperService extends ChopperService { Map? headers, }); + @POST(path: '/auth/jellyfin/quickconnect/initiate') + Future> quickConnectInitiate(); + + @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..65c990b66 100644 --- a/lib/seerr/seerr_models.dart +++ b/lib/seerr/seerr_models.dart @@ -1418,6 +1418,47 @@ 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(), diff --git a/pubspec.lock b/pubspec.lock index 840a9ce2e..2bfc96249 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -1249,18 +1249,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" media_kit: dependency: "direct main" description: @@ -1337,10 +1337,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -2118,10 +2118,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.9" timezone: dependency: transitive description: From b87a747dcd5a874df9ebf3f63345ca6fba17e4da Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:45:32 +0100 Subject: [PATCH 02/63] feat(Seerr): add 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 48e244bcc..9b2e916e8 100644 --- a/lib/providers/seerr_service_provider.dart +++ b/lib/providers/seerr_service_provider.dart @@ -652,6 +652,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({required String username, required String password, Map? headers}) async { From 5b235629fca8e07583b171a2c9ff27f8554772e7 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:52:43 +0100 Subject: [PATCH 03/63] feat(Seerr): add QuickConnect tab to Seerr connection dialog --- lib/l10n/app_en.arb | 2 + .../widgets/seerr_connection_dialog.dart | 146 +++++++++++++++++- 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 33de1926b..946d3c233 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2129,6 +2129,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/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index ccb2d54cc..3d192110c 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'; @@ -26,12 +29,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, }; } @@ -58,6 +63,11 @@ class _SeerrConnectionDialogState extends ConsumerState { bool processing = false; String? error; + // QuickConnect state + String? _qcCode; + String? _qcSecret; + RestartableTimer? _qcTimer; + @override void initState() { super.initState(); @@ -76,6 +86,7 @@ class _SeerrConnectionDialogState extends ConsumerState { @override void dispose() { + _qcTimer?.cancel(); apiKeyController.dispose(); serverController.dispose(); localEmailController.dispose(); @@ -263,6 +274,98 @@ class _SeerrConnectionDialogState extends ConsumerState { } } + Future _quickConnectInitiate() async { + 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(); + final secret = _qcSecret; + if (secret == null) return; + _qcTimer = RestartableTimer(const Duration(seconds: 2), () async { + final authenticated = await ref.read(seerrApiProvider).quickConnectCheck(secret); + if (!mounted) return; + if (authenticated) { + await _quickConnectAuthenticate(secret); + } else { + _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 { _applyServerUrl(showError: false); setState(() { @@ -570,6 +673,47 @@ 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, + ), + 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: [ + 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 36ba48926d55b158196d3aa6e8bc56f2cbb39b57 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:55:34 +0100 Subject: [PATCH 04/63] feat(Seerr): integrate QuickConnect into login flow for passwordless users --- .../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 f0fc3050a..6f5efb08c 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -307,25 +307,32 @@ class _LoginScreenCredentialsState extends ConsumerState Future _tryAuthenticateSeerr(String seerrUrl) async { try { - final username = usernameController.text.trim(); - final password = passwordController.text; - ref.read(userProvider.notifier).setSeerrServerUrl(seerrUrl); 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( @@ -344,7 +351,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 538ddf470d37aed97cf93a50df8ce182d461eb1b Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:01:23 +0100 Subject: [PATCH 05/63] feat: add SEERR_URL env var support to lock Seerr URL --- config/config.json | 5 ++-- lib/models/login_screen_model.dart | 1 + lib/models/login_screen_model.freezed.dart | 27 +++++++++++++++++-- lib/providers/auth_provider.dart | 4 +++ .../login/login_screen_credentials.dart | 2 ++ .../advanced_login_options_dialog.dart | 15 ++++++++--- lib/util/fladder_config.dart | 6 +++++ 7 files changed, 53 insertions(+), 7 deletions(-) diff --git a/config/config.json b/config/config.json index 78ec77596..48ece9f85 100644 --- a/config/config.json +++ b/config/config.json @@ -1,3 +1,4 @@ { - "baseUrl": null -} \ No newline at end of file + "baseUrl": null, + "seerrUrl": null +} diff --git a/lib/models/login_screen_model.dart b/lib/models/login_screen_model.dart index b2f2f4e41..9c41146c9 100644 --- a/lib/models/login_screen_model.dart +++ b/lib/models/login_screen_model.dart @@ -19,6 +19,7 @@ abstract class LoginScreenModel with _$LoginScreenModel { ServerLoginModel? serverLoginModel, String? errorMessage, @Default(false) bool hasBaseUrl, + @Default(false) bool hasSeerrUrl, @Default(false) bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie, diff --git a/lib/models/login_screen_model.freezed.dart b/lib/models/login_screen_model.freezed.dart index 93d679b02..03ee84e99 100644 --- a/lib/models/login_screen_model.freezed.dart +++ b/lib/models/login_screen_model.freezed.dart @@ -19,6 +19,7 @@ mixin _$LoginScreenModel { ServerLoginModel? get serverLoginModel; String? get errorMessage; bool get hasBaseUrl; + bool get hasSeerrUrl; bool get loading; String? get tempSeerrUrl; String? get tempSeerrSessionCookie; @@ -33,7 +34,7 @@ mixin _$LoginScreenModel { @override String toString() { - return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; + return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, hasSeerrUrl: $hasSeerrUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; } } @@ -49,6 +50,7 @@ abstract mixin class $LoginScreenModelCopyWith<$Res> { ServerLoginModel? serverLoginModel, String? errorMessage, bool hasBaseUrl, + bool hasSeerrUrl, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie}); @@ -74,6 +76,7 @@ class _$LoginScreenModelCopyWithImpl<$Res> Object? serverLoginModel = freezed, Object? errorMessage = freezed, Object? hasBaseUrl = null, + Object? hasSeerrUrl = null, Object? loading = null, Object? tempSeerrUrl = freezed, Object? tempSeerrSessionCookie = freezed, @@ -99,6 +102,10 @@ class _$LoginScreenModelCopyWithImpl<$Res> ? _self.hasBaseUrl : hasBaseUrl // ignore: cast_nullable_to_non_nullable as bool, + hasSeerrUrl: null == hasSeerrUrl + ? _self.hasSeerrUrl + : hasSeerrUrl // ignore: cast_nullable_to_non_nullable + as bool, loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable @@ -228,6 +235,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { ServerLoginModel? serverLoginModel, String? errorMessage, bool hasBaseUrl, + bool hasSeerrUrl, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie)? @@ -243,6 +251,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.serverLoginModel, _that.errorMessage, _that.hasBaseUrl, + _that.hasSeerrUrl, _that.loading, _that.tempSeerrUrl, _that.tempSeerrSessionCookie); @@ -272,6 +281,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { ServerLoginModel? serverLoginModel, String? errorMessage, bool hasBaseUrl, + bool hasSeerrUrl, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie) @@ -286,6 +296,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.serverLoginModel, _that.errorMessage, _that.hasBaseUrl, + _that.hasSeerrUrl, _that.loading, _that.tempSeerrUrl, _that.tempSeerrSessionCookie); @@ -314,6 +325,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { ServerLoginModel? serverLoginModel, String? errorMessage, bool hasBaseUrl, + bool hasSeerrUrl, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie)? @@ -328,6 +340,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.serverLoginModel, _that.errorMessage, _that.hasBaseUrl, + _that.hasSeerrUrl, _that.loading, _that.tempSeerrUrl, _that.tempSeerrSessionCookie); @@ -346,6 +359,7 @@ class _LoginScreenModel implements LoginScreenModel { this.serverLoginModel, this.errorMessage, this.hasBaseUrl = false, + this.hasSeerrUrl = false, this.loading = false, this.tempSeerrUrl, this.tempSeerrSessionCookie}) @@ -372,6 +386,9 @@ class _LoginScreenModel implements LoginScreenModel { final bool hasBaseUrl; @override @JsonKey() + final bool hasSeerrUrl; + @override + @JsonKey() final bool loading; @override final String? tempSeerrUrl; @@ -388,7 +405,7 @@ class _LoginScreenModel implements LoginScreenModel { @override String toString() { - return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; + return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, hasSeerrUrl: $hasSeerrUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; } } @@ -406,6 +423,7 @@ abstract mixin class _$LoginScreenModelCopyWith<$Res> ServerLoginModel? serverLoginModel, String? errorMessage, bool hasBaseUrl, + bool hasSeerrUrl, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie}); @@ -432,6 +450,7 @@ class __$LoginScreenModelCopyWithImpl<$Res> Object? serverLoginModel = freezed, Object? errorMessage = freezed, Object? hasBaseUrl = null, + Object? hasSeerrUrl = null, Object? loading = null, Object? tempSeerrUrl = freezed, Object? tempSeerrSessionCookie = freezed, @@ -457,6 +476,10 @@ class __$LoginScreenModelCopyWithImpl<$Res> ? _self.hasBaseUrl : hasBaseUrl // ignore: cast_nullable_to_non_nullable as bool, + hasSeerrUrl: null == hasSeerrUrl + ? _self.hasSeerrUrl + : hasSeerrUrl // ignore: cast_nullable_to_non_nullable + as bool, loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index ad5cd29df..ca52985e8 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -50,6 +50,10 @@ class AuthNotifier extends StateNotifier { await setServer(url); } } + if (FladderConfig.seerrUrl != null) { + state = state.copyWith(hasSeerrUrl: true); + setTempSeerrUrl(FladderConfig.seerrUrl); + } state = state.copyWith( accounts: currentAccounts, screen: currentAccounts.isEmpty ? LoginScreenType.login : LoginScreenType.users, diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 6f5efb08c..14e6397e7 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -209,9 +209,11 @@ class _LoginScreenCredentialsState extends ConsumerState IconButton.filledTonal( onPressed: () async { final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); final result = await showAdvancedLoginOptionsDialog( context, initialSeerrUrl: tempSeerrUrl, + hasSeerrUrl: hasSeerrUrl, ); if (result != null) { ref.read(authProvider.notifier).setTempSeerrUrl(result); diff --git a/lib/screens/login/widgets/advanced_login_options_dialog.dart b/lib/screens/login/widgets/advanced_login_options_dialog.dart index 6c2373154..d0a0efb83 100644 --- a/lib/screens/login/widgets/advanced_login_options_dialog.dart +++ b/lib/screens/login/widgets/advanced_login_options_dialog.dart @@ -7,17 +7,25 @@ import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; -Future showAdvancedLoginOptionsDialog(BuildContext context, {String? initialSeerrUrl}) async { +Future showAdvancedLoginOptionsDialog( + BuildContext context, { + String? initialSeerrUrl, + bool hasSeerrUrl = false, +}) async { return await showDialog( context: context, - builder: (context) => _AdvancedLoginOptionsDialog(initialSeerrUrl: initialSeerrUrl), + builder: (context) => _AdvancedLoginOptionsDialog( + initialSeerrUrl: initialSeerrUrl, + hasSeerrUrl: hasSeerrUrl, + ), ); } class _AdvancedLoginOptionsDialog extends ConsumerStatefulWidget { final String? initialSeerrUrl; + final bool hasSeerrUrl; - const _AdvancedLoginOptionsDialog({this.initialSeerrUrl}); + const _AdvancedLoginOptionsDialog({this.initialSeerrUrl, this.hasSeerrUrl = false}); @override ConsumerState<_AdvancedLoginOptionsDialog> createState() => _AdvancedLoginOptionsDialogState(); @@ -55,6 +63,7 @@ class _AdvancedLoginOptionsDialogState extends ConsumerState<_AdvancedLoginOptio textInputAction: TextInputAction.done, autoFillHints: const [AutofillHints.url], autocorrect: false, + enabled: !widget.hasSeerrUrl, label: context.localized.seerrServer, onSubmitted: (_) => _save(), ), diff --git a/lib/util/fladder_config.dart b/lib/util/fladder_config.dart index e54040fda..2906d0005 100644 --- a/lib/util/fladder_config.dart +++ b/lib/util/fladder_config.dart @@ -6,12 +6,18 @@ class FladderConfig { static set baseUrl(String? value) => _instance._baseUrl = value; String? _baseUrl; + static String? get seerrUrl => _instance._seerrUrl; + static set seerrUrl(String? value) => _instance._seerrUrl = value; + String? _seerrUrl; + static void fromJson(Map json) => _instance = FladderConfig._fromJson(json); factory FladderConfig._fromJson(Map json) { final config = FladderConfig._(); final newUrl = json['baseUrl'] as String?; config._baseUrl = newUrl?.isEmpty == true ? null : newUrl; + final newSeerrUrl = json['seerrUrl'] as String?; + config._seerrUrl = newSeerrUrl?.isEmpty == true ? null : newSeerrUrl; return config; } } From 8b7109f9f02f075feba41567b637bef5c083c024 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:06:35 +0100 Subject: [PATCH 06/63] feat: add toggle to hide password login fields --- config/config.json | 3 +- lib/l10n/app_en.arb | 4 + lib/models/login_screen_model.dart | 1 + lib/models/login_screen_model.freezed.dart | 27 +++- .../settings/client_settings_model.dart | 1 + .../client_settings_model.freezed.dart | 29 ++++- .../settings/client_settings_model.g.dart | 2 + lib/providers/auth_provider.dart | 3 + .../login/login_screen_credentials.dart | 118 +++++++++--------- .../client_settings_advanced.dart | 13 ++ lib/util/fladder_config.dart | 5 + 11 files changed, 145 insertions(+), 61 deletions(-) diff --git a/config/config.json b/config/config.json index 48ece9f85..c08043932 100644 --- a/config/config.json +++ b/config/config.json @@ -1,4 +1,5 @@ { "baseUrl": null, - "seerrUrl": null + "seerrUrl": null, + "hidePasswordLogin": null } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 946d3c233..c7a6e4eff 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2131,6 +2131,10 @@ "@seerrAuthJellyfin": {}, "seerrAuthQuickConnect": "QuickConnect", "@seerrAuthQuickConnect": {}, + "hidePasswordLogin": "Hide password login", + "@hidePasswordLogin": {}, + "hidePasswordLoginDescription": "Only show QuickConnect on the login screen", + "@hidePasswordLoginDescription": {}, "seerrUserFetchFailed": "Failed to fetch user from Seerr", "@seerrUserFetchFailed": {}, "seerrEnterServerUrlFirst": "Enter a Seerr server URL first", diff --git a/lib/models/login_screen_model.dart b/lib/models/login_screen_model.dart index 9c41146c9..55ce65199 100644 --- a/lib/models/login_screen_model.dart +++ b/lib/models/login_screen_model.dart @@ -20,6 +20,7 @@ abstract class LoginScreenModel with _$LoginScreenModel { String? errorMessage, @Default(false) bool hasBaseUrl, @Default(false) bool hasSeerrUrl, + @Default(false) bool hidePasswordLogin, @Default(false) bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie, diff --git a/lib/models/login_screen_model.freezed.dart b/lib/models/login_screen_model.freezed.dart index 03ee84e99..a4cdcabdc 100644 --- a/lib/models/login_screen_model.freezed.dart +++ b/lib/models/login_screen_model.freezed.dart @@ -20,6 +20,7 @@ mixin _$LoginScreenModel { String? get errorMessage; bool get hasBaseUrl; bool get hasSeerrUrl; + bool get hidePasswordLogin; bool get loading; String? get tempSeerrUrl; String? get tempSeerrSessionCookie; @@ -34,7 +35,7 @@ mixin _$LoginScreenModel { @override String toString() { - return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, hasSeerrUrl: $hasSeerrUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; + return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, hasSeerrUrl: $hasSeerrUrl, hidePasswordLogin: $hidePasswordLogin, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; } } @@ -51,6 +52,7 @@ abstract mixin class $LoginScreenModelCopyWith<$Res> { String? errorMessage, bool hasBaseUrl, bool hasSeerrUrl, + bool hidePasswordLogin, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie}); @@ -77,6 +79,7 @@ class _$LoginScreenModelCopyWithImpl<$Res> Object? errorMessage = freezed, Object? hasBaseUrl = null, Object? hasSeerrUrl = null, + Object? hidePasswordLogin = null, Object? loading = null, Object? tempSeerrUrl = freezed, Object? tempSeerrSessionCookie = freezed, @@ -106,6 +109,10 @@ class _$LoginScreenModelCopyWithImpl<$Res> ? _self.hasSeerrUrl : hasSeerrUrl // ignore: cast_nullable_to_non_nullable as bool, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable @@ -236,6 +243,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { String? errorMessage, bool hasBaseUrl, bool hasSeerrUrl, + bool hidePasswordLogin, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie)? @@ -252,6 +260,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.errorMessage, _that.hasBaseUrl, _that.hasSeerrUrl, + _that.hidePasswordLogin, _that.loading, _that.tempSeerrUrl, _that.tempSeerrSessionCookie); @@ -282,6 +291,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { String? errorMessage, bool hasBaseUrl, bool hasSeerrUrl, + bool hidePasswordLogin, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie) @@ -297,6 +307,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.errorMessage, _that.hasBaseUrl, _that.hasSeerrUrl, + _that.hidePasswordLogin, _that.loading, _that.tempSeerrUrl, _that.tempSeerrSessionCookie); @@ -326,6 +337,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { String? errorMessage, bool hasBaseUrl, bool hasSeerrUrl, + bool hidePasswordLogin, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie)? @@ -341,6 +353,7 @@ extension LoginScreenModelPatterns on LoginScreenModel { _that.errorMessage, _that.hasBaseUrl, _that.hasSeerrUrl, + _that.hidePasswordLogin, _that.loading, _that.tempSeerrUrl, _that.tempSeerrSessionCookie); @@ -360,6 +373,7 @@ class _LoginScreenModel implements LoginScreenModel { this.errorMessage, this.hasBaseUrl = false, this.hasSeerrUrl = false, + this.hidePasswordLogin = false, this.loading = false, this.tempSeerrUrl, this.tempSeerrSessionCookie}) @@ -389,6 +403,9 @@ class _LoginScreenModel implements LoginScreenModel { final bool hasSeerrUrl; @override @JsonKey() + final bool hidePasswordLogin; + @override + @JsonKey() final bool loading; @override final String? tempSeerrUrl; @@ -405,7 +422,7 @@ class _LoginScreenModel implements LoginScreenModel { @override String toString() { - return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, hasSeerrUrl: $hasSeerrUrl, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; + return 'LoginScreenModel(accounts: $accounts, screen: $screen, serverLoginModel: $serverLoginModel, errorMessage: $errorMessage, hasBaseUrl: $hasBaseUrl, hasSeerrUrl: $hasSeerrUrl, hidePasswordLogin: $hidePasswordLogin, loading: $loading, tempSeerrUrl: $tempSeerrUrl, tempSeerrSessionCookie: $tempSeerrSessionCookie)'; } } @@ -424,6 +441,7 @@ abstract mixin class _$LoginScreenModelCopyWith<$Res> String? errorMessage, bool hasBaseUrl, bool hasSeerrUrl, + bool hidePasswordLogin, bool loading, String? tempSeerrUrl, String? tempSeerrSessionCookie}); @@ -451,6 +469,7 @@ class __$LoginScreenModelCopyWithImpl<$Res> Object? errorMessage = freezed, Object? hasBaseUrl = null, Object? hasSeerrUrl = null, + Object? hidePasswordLogin = null, Object? loading = null, Object? tempSeerrUrl = freezed, Object? tempSeerrSessionCookie = freezed, @@ -480,6 +499,10 @@ class __$LoginScreenModelCopyWithImpl<$Res> ? _self.hasSeerrUrl : hasSeerrUrl // ignore: cast_nullable_to_non_nullable as bool, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index ecbbf51cc..a0b5568a9 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -89,6 +89,7 @@ abstract class ClientSettingsModel with _$ClientSettingsModel { @Default(false) bool usePosterForLibrary, @Default(false) bool useSystemIME, @Default(false) bool useTVExpandedLayout, + @Default(false) bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, @Default({}) Map shortcuts, diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index 5f5913736..a8fa6015e 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -43,6 +43,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin { bool get usePosterForLibrary; bool get useSystemIME; bool get useTVExpandedLayout; + bool get hidePasswordLogin; String? get lastViewedUpdate; int? get libraryPageSize; Map get shortcuts; @@ -93,6 +94,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary)) ..add(DiagnosticsProperty('useSystemIME', useSystemIME)) ..add(DiagnosticsProperty('useTVExpandedLayout', useTVExpandedLayout)) + ..add(DiagnosticsProperty('hidePasswordLogin', hidePasswordLogin)) ..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate)) ..add(DiagnosticsProperty('libraryPageSize', libraryPageSize)) ..add(DiagnosticsProperty('shortcuts', shortcuts)); @@ -100,7 +102,7 @@ mixin _$ClientSettingsModel implements DiagnosticableTreeMixin { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; + return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, hidePasswordLogin: $hidePasswordLogin, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; } } @@ -139,6 +141,7 @@ abstract mixin class $ClientSettingsModelCopyWith<$Res> { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts}); @@ -185,6 +188,7 @@ class _$ClientSettingsModelCopyWithImpl<$Res> Object? usePosterForLibrary = null, Object? useSystemIME = null, Object? useTVExpandedLayout = null, + Object? hidePasswordLogin = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, Object? shortcuts = null, @@ -302,6 +306,10 @@ class _$ClientSettingsModelCopyWithImpl<$Res> ? _self.useTVExpandedLayout : useTVExpandedLayout // ignore: cast_nullable_to_non_nullable as bool, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _self.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable @@ -440,6 +448,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts)? @@ -478,6 +487,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { _that.usePosterForLibrary, _that.useSystemIME, _that.useTVExpandedLayout, + _that.hidePasswordLogin, _that.lastViewedUpdate, _that.libraryPageSize, _that.shortcuts); @@ -530,6 +540,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts) @@ -567,6 +578,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { _that.usePosterForLibrary, _that.useSystemIME, _that.useTVExpandedLayout, + _that.hidePasswordLogin, _that.lastViewedUpdate, _that.libraryPageSize, _that.shortcuts); @@ -618,6 +630,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts)? @@ -655,6 +668,7 @@ extension ClientSettingsModelPatterns on ClientSettingsModel { _that.usePosterForLibrary, _that.useSystemIME, _that.useTVExpandedLayout, + _that.hidePasswordLogin, _that.lastViewedUpdate, _that.libraryPageSize, _that.shortcuts); @@ -697,6 +711,7 @@ class _ClientSettingsModel extends ClientSettingsModel this.usePosterForLibrary = false, this.useSystemIME = false, this.useTVExpandedLayout = false, + this.hidePasswordLogin = false, this.lastViewedUpdate, this.libraryPageSize, final Map shortcuts = const {}}) @@ -787,6 +802,9 @@ class _ClientSettingsModel extends ClientSettingsModel @JsonKey() final bool useTVExpandedLayout; @override + @JsonKey() + final bool hidePasswordLogin; + @override final String? lastViewedUpdate; @override final int? libraryPageSize; @@ -850,6 +868,7 @@ class _ClientSettingsModel extends ClientSettingsModel ..add(DiagnosticsProperty('usePosterForLibrary', usePosterForLibrary)) ..add(DiagnosticsProperty('useSystemIME', useSystemIME)) ..add(DiagnosticsProperty('useTVExpandedLayout', useTVExpandedLayout)) + ..add(DiagnosticsProperty('hidePasswordLogin', hidePasswordLogin)) ..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate)) ..add(DiagnosticsProperty('libraryPageSize', libraryPageSize)) ..add(DiagnosticsProperty('shortcuts', shortcuts)); @@ -857,7 +876,7 @@ class _ClientSettingsModel extends ClientSettingsModel @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; + return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, updateNotificationsInterval: $updateNotificationsInterval, themeMode: $themeMode, themeColor: $themeColor, deriveColorsFromItem: $deriveColorsFromItem, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, expandSideBar: $expandSideBar, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundImage: $backgroundImage, enableBlurEffects: $enableBlurEffects, checkForUpdates: $checkForUpdates, usePosterForLibrary: $usePosterForLibrary, useSystemIME: $useSystemIME, useTVExpandedLayout: $useTVExpandedLayout, hidePasswordLogin: $hidePasswordLogin, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize, shortcuts: $shortcuts)'; } } @@ -898,6 +917,7 @@ abstract mixin class _$ClientSettingsModelCopyWith<$Res> bool usePosterForLibrary, bool useSystemIME, bool useTVExpandedLayout, + bool hidePasswordLogin, String? lastViewedUpdate, int? libraryPageSize, Map shortcuts}); @@ -944,6 +964,7 @@ class __$ClientSettingsModelCopyWithImpl<$Res> Object? usePosterForLibrary = null, Object? useSystemIME = null, Object? useTVExpandedLayout = null, + Object? hidePasswordLogin = null, Object? lastViewedUpdate = freezed, Object? libraryPageSize = freezed, Object? shortcuts = null, @@ -1061,6 +1082,10 @@ class __$ClientSettingsModelCopyWithImpl<$Res> ? _self.useTVExpandedLayout : useTVExpandedLayout // ignore: cast_nullable_to_non_nullable as bool, + hidePasswordLogin: null == hidePasswordLogin + ? _self.hidePasswordLogin + : hidePasswordLogin // ignore: cast_nullable_to_non_nullable + as bool, lastViewedUpdate: freezed == lastViewedUpdate ? _self.lastViewedUpdate : lastViewedUpdate // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index 507790c3c..2504a1b22 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -55,6 +55,7 @@ _ClientSettingsModel _$ClientSettingsModelFromJson(Map json) => usePosterForLibrary: json['usePosterForLibrary'] as bool? ?? false, useSystemIME: json['useSystemIME'] as bool? ?? false, useTVExpandedLayout: json['useTVExpandedLayout'] as bool? ?? false, + hidePasswordLogin: json['hidePasswordLogin'] as bool? ?? false, lastViewedUpdate: json['lastViewedUpdate'] as String?, libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(), shortcuts: (json['shortcuts'] as Map?)?.map( @@ -96,6 +97,7 @@ Map _$ClientSettingsModelToJson( 'usePosterForLibrary': instance.usePosterForLibrary, 'useSystemIME': instance.useSystemIME, 'useTVExpandedLayout': instance.useTVExpandedLayout, + 'hidePasswordLogin': instance.hidePasswordLogin, 'lastViewedUpdate': instance.lastViewedUpdate, 'libraryPageSize': instance.libraryPageSize, 'shortcuts': instance.shortcuts diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index ca52985e8..978660ae0 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -54,6 +54,9 @@ class AuthNotifier extends StateNotifier { state = state.copyWith(hasSeerrUrl: true); setTempSeerrUrl(FladderConfig.seerrUrl); } + if (FladderConfig.hidePasswordLogin == true) { + state = state.copyWith(hidePasswordLogin: true); + } state = state.copyWith( accounts: currentAccounts, screen: currentAccounts.isEmpty ? LoginScreenType.login : LoginScreenType.users, diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 14e6397e7..5d25f1dcf 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -10,6 +10,7 @@ import 'package:fladder/models/account_model.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/providers/seerr_api_provider.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -60,6 +61,8 @@ class _LoginScreenCredentialsState extends ConsumerState final hasBaseUrl = ref.watch(authProvider.select((value) => value.hasBaseUrl)); final urlError = ref.watch(authProvider.select((value) => value.errorMessage)); final hasQuickConnect = ref.watch(authProvider.select((value) => value.serverLoginModel?.hasQuickConnect ?? false)); + final hidePasswordLogin = ref.watch(authProvider.select((value) => value.hidePasswordLogin)) || + ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)); ref.listen( authProvider.select((value) => value.serverLoginModel), @@ -156,73 +159,76 @@ class _LoginScreenCredentialsState extends ConsumerState child: OutlinedTextField( controller: usernameController, autoFillHints: const [AutofillHints.username], - textInputAction: TextInputAction.next, + textInputAction: hidePasswordLogin ? TextInputAction.done : TextInputAction.next, autocorrect: false, onChanged: (value) => setState(() {}), label: context.localized.userName, ), ), - Flexible( - child: OutlinedTextField( - controller: passwordController, - autoFillHints: const [AutofillHints.password], - keyboardType: TextInputType.visiblePassword, - focusNode: focusNode, - autocorrect: false, - textInputAction: TextInputAction.send, - onSubmitted: (value) => enterCredentialsTryLogin?.call(), - onChanged: (value) => setState(() {}), - label: context.localized.password, + if (!hidePasswordLogin) + Flexible( + child: OutlinedTextField( + controller: passwordController, + autoFillHints: const [AutofillHints.password], + keyboardType: TextInputType.visiblePassword, + focusNode: focusNode, + autocorrect: false, + textInputAction: TextInputAction.send, + onSubmitted: (value) => enterCredentialsTryLogin?.call(), + onChanged: (value) => setState(() {}), + label: context.localized.password, + ), ), - ), ], ), ), ), - const Divider( - indent: 32, - endIndent: 32, - ), - Row( - spacing: 8, - children: [ - Expanded( - child: FilledButton( - onPressed: enterCredentialsTryLogin, - child: loggingIn - ? SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.inversePrimary, strokeCap: StrokeCap.round), - ) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(context.localized.login), - const SizedBox(width: 8), - const Icon(IconsaxPlusBold.send_1), - ], - ), + if (!hidePasswordLogin) ...[ + const Divider( + indent: 32, + endIndent: 32, + ), + Row( + spacing: 8, + children: [ + Expanded( + child: FilledButton( + onPressed: enterCredentialsTryLogin, + child: loggingIn + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + color: Theme.of(context).colorScheme.inversePrimary, strokeCap: StrokeCap.round), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.localized.login), + const SizedBox(width: 8), + const Icon(IconsaxPlusBold.send_1), + ], + ), + ), ), - ), - IconButton.filledTonal( - onPressed: () async { - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); - final result = await showAdvancedLoginOptionsDialog( - context, - initialSeerrUrl: tempSeerrUrl, - hasSeerrUrl: hasSeerrUrl, - ); - if (result != null) { - ref.read(authProvider.notifier).setTempSeerrUrl(result); - } - }, - icon: const Icon(IconsaxPlusLinear.setting_3), - ), - ], - ), + IconButton.filledTonal( + onPressed: () async { + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); + final result = await showAdvancedLoginOptionsDialog( + context, + initialSeerrUrl: tempSeerrUrl, + hasSeerrUrl: hasSeerrUrl, + ); + if (result != null) { + ref.read(authProvider.notifier).setTempSeerrUrl(result); + } + }, + icon: const Icon(IconsaxPlusLinear.setting_3), + ), + ], + ), + ], if (hasQuickConnect) FilledButton( onPressed: () async { diff --git a/lib/screens/settings/client_sections/client_settings_advanced.dart b/lib/screens/settings/client_sections/client_settings_advanced.dart index b0e04ed08..aed6bf12c 100644 --- a/lib/screens/settings/client_sections/client_settings_advanced.dart +++ b/lib/screens/settings/client_sections/client_settings_advanced.dart @@ -115,6 +115,19 @@ List buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) { onChanged: (value) => ref.read(clientSettingsProvider.notifier).useSystemIME(value), ), ), + SettingsListTile( + label: Text(context.localized.hidePasswordLogin), + subLabel: Text(context.localized.hidePasswordLoginDescription), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(hidePasswordLogin: !current.hidePasswordLogin)), + trailing: Switch( + value: ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)), + onChanged: (value) => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(hidePasswordLogin: value)), + ), + ), ], ); } diff --git a/lib/util/fladder_config.dart b/lib/util/fladder_config.dart index 2906d0005..8c47edbf7 100644 --- a/lib/util/fladder_config.dart +++ b/lib/util/fladder_config.dart @@ -10,6 +10,10 @@ class FladderConfig { static set seerrUrl(String? value) => _instance._seerrUrl = value; String? _seerrUrl; + static bool? get hidePasswordLogin => _instance._hidePasswordLogin; + static set hidePasswordLogin(bool? value) => _instance._hidePasswordLogin = value; + bool? _hidePasswordLogin; + static void fromJson(Map json) => _instance = FladderConfig._fromJson(json); factory FladderConfig._fromJson(Map json) { @@ -18,6 +22,7 @@ class FladderConfig { config._baseUrl = newUrl?.isEmpty == true ? null : newUrl; final newSeerrUrl = json['seerrUrl'] as String?; config._seerrUrl = newSeerrUrl?.isEmpty == true ? null : newSeerrUrl; + config._hidePasswordLogin = json['hidePasswordLogin'] as bool?; return config; } } From d868c0958ae60d6c03de312783887932e6e6b415 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:12:07 +0100 Subject: [PATCH 07/63] fix(ci): build debug APK to bypass keystore signing issue --- .github/workflows/build.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39715280d..7f8f5e0f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: - "v*" branches: - master + - feat/seerr-quickconnect pull_request: paths: - pubspec.yaml @@ -173,25 +174,44 @@ jobs: run: | flutter build apk --debug --build-number=${{github.run_number}} --flavor production - - name: Build Android APK and AAB - if: needs.fetch-info.outputs.build_type != 'development' + - name: Build Android APK and AAB (signed release) + if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING != '' + env: + ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} run: | flutter build apk --release --build-number=${{github.run_number}} --flavor production flutter build appbundle --release --build-number=${{github.run_number}} --flavor production + - name: Build Android APK (fallback debug) + if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING == '' + env: + ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} + run: | + flutter build apk --debug --build-number=${{github.run_number}} --flavor production + - name: Rename APK for PR if: needs.fetch-info.outputs.build_type == 'development' run: | mkdir -p build/app/outputs/android_artifacts mv build/app/outputs/flutter-apk/app-production-debug.apk "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.apk" - - name: Rename APK and AAB - if: needs.fetch-info.outputs.build_type != 'development' + - name: Rename APK and AAB (signed release) + if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING != '' + env: + ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} run: | mkdir -p build/app/outputs/android_artifacts mv build/app/outputs/flutter-apk/app-production-release.apk "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.apk" mv build/app/outputs/bundle/productionRelease/app-production-release.aab "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.aab" + - name: Rename APK (fallback debug) + if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING == '' + env: + ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} + run: | + mkdir -p build/app/outputs/android_artifacts + mv build/app/outputs/flutter-apk/app-production-debug.apk "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.apk" + - name: Archive Android artifacts uses: actions/upload-artifact@v4.0.0 with: From 76972020956c305867c6871ca90ab6a5561cf4a7 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:39:54 +0100 Subject: [PATCH 08/63] fix(login): hide username field when hidePasswordLogin is enabled --- .../login/login_screen_credentials.dart | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 5d25f1dcf..4f3316c34 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -148,24 +148,24 @@ class _LoginScreenCredentialsState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.stretch, spacing: 8, children: [ - Flexible( - child: AutofillGroup( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - spacing: 8, - children: [ - Flexible( - child: OutlinedTextField( - controller: usernameController, - autoFillHints: const [AutofillHints.username], - textInputAction: hidePasswordLogin ? TextInputAction.done : TextInputAction.next, - autocorrect: false, - onChanged: (value) => setState(() {}), - label: context.localized.userName, + if (!hidePasswordLogin) + Flexible( + child: AutofillGroup( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + Flexible( + child: OutlinedTextField( + controller: usernameController, + autoFillHints: const [AutofillHints.username], + textInputAction: TextInputAction.next, + autocorrect: false, + onChanged: (value) => setState(() {}), + label: context.localized.userName, + ), ), - ), - if (!hidePasswordLogin) Flexible( child: OutlinedTextField( controller: passwordController, @@ -179,10 +179,10 @@ class _LoginScreenCredentialsState extends ConsumerState label: context.localized.password, ), ), - ], + ], + ), ), ), - ), if (!hidePasswordLogin) ...[ const Divider( indent: 32, From 38ab1c716eb661fc9afa9922350b1271276476be Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:41:43 +0100 Subject: [PATCH 09/63] fix(Seerr): stretch QuickConnect tab and center action button --- .../widgets/seerr_connection_dialog.dart | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 3d192110c..1ae066005 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -547,22 +547,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()), @@ -704,11 +707,13 @@ class _SeerrConnectionDialogState extends ConsumerState { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - FilledButton( - onPressed: processing ? null : _quickConnectInitiate, - child: processing - ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) - : Text(_qcCode != null ? context.localized.refresh : context.localized.quickConnectTitle), + 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 0adff20fa9cd62e3a12a438b5522f33758a0ef31 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:42:33 +0100 Subject: [PATCH 10/63] feat(login): copy QuickConnect code to clipboard on tap --- lib/screens/login/login_code_dialog.dart | 32 +++++++++++-------- .../widgets/seerr_connection_dialog.dart | 28 +++++++++------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/lib/screens/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index 3d93b999a..21b4dc159 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -5,6 +5,7 @@ import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/auth_provider.dart'; import 'package:fladder/util/list_padding.dart'; +import 'package:fladder/util/clipboard_helper.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -102,20 +103,23 @@ class _LoginCodeDialogState extends ConsumerState { style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), - IntrinsicWidth( - child: Card( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Text( - code, - style: - Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - wordSpacing: 8, - letterSpacing: 8, - ), - textAlign: TextAlign.center, - semanticsLabel: code, + GestureDetector( + onTap: () => context.copyToClipboard(code), + child: IntrinsicWidth( + child: Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + code, + style: + Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + wordSpacing: 8, + letterSpacing: 8, + ), + textAlign: TextAlign.center, + semanticsLabel: code, + ), ), ), ), diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 1ae066005..26a84be96 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -17,6 +17,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/localization_helper.dart'; Future showSeerrConnectionDialog(BuildContext context) { @@ -687,18 +688,21 @@ class _SeerrConnectionDialogState extends ConsumerState { style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), - 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, + 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, + ), ), ), ), From 84875d957d3b1c69294b067b76d7ab24652cd7eb Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:44:26 +0100 Subject: [PATCH 11/63] feat(login): add "Open Jellyfin QuickConnect" link in QC dialogs --- lib/l10n/app_en.arb | 2 ++ lib/screens/login/login_code_dialog.dart | 16 +++++++++++++++- .../widgets/seerr_connection_dialog.dart | 13 +++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c7a6e4eff..888097fa0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1525,6 +1525,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/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index 21b4dc159..e883c9d6b 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -4,11 +4,14 @@ import 'package:async/async.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/auth_provider.dart'; -import 'package:fladder/util/list_padding.dart'; +import 'package:fladder/screens/shared/media/external_urls.dart' as ext; import 'package:fladder/util/clipboard_helper.dart'; +import 'package:fladder/util/fladder_config.dart'; +import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; Future openLoginCodeDialog( BuildContext context, { @@ -124,6 +127,17 @@ class _LoginCodeDialogState extends ConsumerState { ), ), ), + TextButton.icon( + onPressed: () { + final baseUrl = FladderConfig.baseUrl ?? + ref.read(authProvider).serverLoginModel?.tempCredentials.url; + if (baseUrl != null && baseUrl.isNotEmpty) { + ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + } + }, + icon: const Icon(IconsaxPlusLinear.export_1), + label: Text(context.localized.openJellyfinQuickConnect), + ), ], FilledButton( onPressed: () async { diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 26a84be96..892ca94e4 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -12,12 +12,14 @@ import 'package:fladder/providers/seerr_dashboard_provider.dart'; import 'package:fladder/providers/seerr_user_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/adaptive_dialog.dart'; +import 'package:fladder/screens/shared/media/external_urls.dart' as ext; import 'package:fladder/screens/shared/animated_fade_size.dart'; 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'; Future showSeerrConnectionDialog(BuildContext context) { @@ -707,6 +709,17 @@ class _SeerrConnectionDialogState extends ConsumerState { ), ), ), + TextButton.icon( + onPressed: () { + final baseUrl = FladderConfig.baseUrl ?? + ref.read(userProvider)?.credentials.url; + if (baseUrl != null && baseUrl.isNotEmpty) { + ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + } + }, + icon: const Icon(IconsaxPlusLinear.export_1), + label: Text(context.localized.openJellyfinQuickConnect), + ), ], Row( mainAxisAlignment: MainAxisAlignment.end, From 12134098e8019520a667d407b24f8d6ab7e253af Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:44:58 +0100 Subject: [PATCH 12/63] fix(l10n): rename "Local Server URL" to "Local Jellyfin URL" --- lib/l10n/app_en.arb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 888097fa0..1d9c70167 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1565,9 +1565,9 @@ "screensaverBlack": "Black", "playerSettingsScreensaverTitle": "Screensaver", "playerSettingsScreensaverDesc": "Select screen saver that will be shown after no player activity", - "settingsLocalUrlTitle": "Local Server URL", - "settingsLocalUrlSetTitle": "Configure Local URL", - "settingsLocalUrlSetDesc": "Specify the local server address. Fladder will automatically use this URL when your device is on the same network.", + "settingsLocalUrlTitle": "Local Jellyfin URL", + "settingsLocalUrlSetTitle": "Configure Local Jellyfin URL", + "settingsLocalUrlSetDesc": "Specify the local Jellyfin address. Fladder will automatically use this URL when your device is on the same network.", "regenerateTrickplayImages": "Regenerate trickplay images", "controlPanel": "Control Panel", "@controlPanel": {}, From c5ec30bf19511472b97f5140a73c40388d28369f Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:46:54 +0100 Subject: [PATCH 13/63] fix(ci): run Docker deploy for development builds --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7f8f5e0f2..7806eaf98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -719,7 +719,7 @@ jobs: - fetch-info - create_release runs-on: ubuntu-latest - if: needs.fetch-info.outputs.build_type == 'release' + if: needs.fetch-info.outputs.build_type == 'release' || needs.fetch-info.outputs.build_type == 'development' steps: - name: Checkout repository uses: actions/checkout@v4.1.1 @@ -754,15 +754,18 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Clean builds folder + if: needs.fetch-info.outputs.build_type == 'release' run: rm -rf build/web - name: Download Artifacts Web + if: needs.fetch-info.outputs.build_type == 'release' uses: actions/download-artifact@v4 with: name: fladder-web-pages path: build/web - name: Deploy to GitHub Pages + if: needs.fetch-info.outputs.build_type == 'release' uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} From e5249c5250a9b02bf24d8ae79fb5055740fbe2a0 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 03:22:53 +0100 Subject: [PATCH 14/63] fix(login): restart QC polling after in-app browser closes in login dialog --- lib/screens/login/login_code_dialog.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/screens/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index e883c9d6b..ab09bd995 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -128,11 +128,12 @@ class _LoginCodeDialogState extends ConsumerState { ), ), TextButton.icon( - onPressed: () { + onPressed: () async { final baseUrl = FladderConfig.baseUrl ?? ref.read(authProvider).serverLoginModel?.tempCredentials.url; if (baseUrl != null && baseUrl.isNotEmpty) { - ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + await ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + timer?.reset(); } }, icon: const Icon(IconsaxPlusLinear.export_1), From 39f54c7f5e7180803be6e9ca79c550b8383fed8d Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 03:24:06 +0100 Subject: [PATCH 15/63] fix(login): restart QC polling after in-app browser closes --- lib/screens/settings/widgets/seerr_connection_dialog.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 892ca94e4..ac27d740e 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -710,11 +710,12 @@ class _SeerrConnectionDialogState extends ConsumerState { ), ), TextButton.icon( - onPressed: () { + onPressed: () async { final baseUrl = FladderConfig.baseUrl ?? ref.read(userProvider)?.credentials.url; if (baseUrl != null && baseUrl.isNotEmpty) { - ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + await ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + _qcTimer?.reset(); } }, icon: const Icon(IconsaxPlusLinear.export_1), From 338b63b885b4192fd1ec1e142ad252e6defc623a Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:26:38 +0100 Subject: [PATCH 16/63] fix(login): prevent QC polling from silently stopping on errors --- lib/screens/login/login_code_dialog.dart | 22 +++++++++++-------- .../widgets/seerr_connection_dialog.dart | 16 +++++++++----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/screens/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index ab09bd995..8f0804f07 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -60,15 +60,19 @@ class _LoginCodeDialogState extends ConsumerState { void createTimer() { timer?.cancel(); 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 { + 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/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index ac27d740e..750d9d079 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -318,12 +318,16 @@ class _SeerrConnectionDialogState extends ConsumerState { final secret = _qcSecret; if (secret == null) return; _qcTimer = RestartableTimer(const Duration(seconds: 2), () async { - final authenticated = await ref.read(seerrApiProvider).quickConnectCheck(secret); - if (!mounted) return; - if (authenticated) { - await _quickConnectAuthenticate(secret); - } else { - _qcTimer?.reset(); + 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(); } }); } From 821c91f57fa1bf8dff19df08da8a0d1410299e6d Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:27:31 +0100 Subject: [PATCH 17/63] fix(login): show settings gear when password login is hidden --- .../login/login_screen_credentials.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 4f3316c34..2766de792 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -229,6 +229,27 @@ class _LoginScreenCredentialsState extends ConsumerState ], ), ], + if (hidePasswordLogin) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton.filledTonal( + onPressed: () async { + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); + final result = await showAdvancedLoginOptionsDialog( + context, + initialSeerrUrl: tempSeerrUrl, + hasSeerrUrl: hasSeerrUrl, + ); + if (result != null) { + ref.read(authProvider.notifier).setTempSeerrUrl(result); + } + }, + icon: const Icon(IconsaxPlusLinear.setting_3), + ), + ], + ), if (hasQuickConnect) FilledButton( onPressed: () async { From ffcf03f073dea6c9411b466344159cfaf75413b6 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:29:28 +0100 Subject: [PATCH 18/63] fix(login): auto-add URL scheme for Jellyfin and Seerr URLs --- lib/providers/auth_provider.dart | 18 +++++++++++++++--- .../widgets/advanced_login_options_dialog.dart | 4 +++- .../widgets/seerr_connection_dialog.dart | 9 +++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 978660ae0..a4ce12f51 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -196,9 +196,21 @@ class AuthNotifier extends StateNotifier { } Future setServer(String server) async { - final url = (state.hasBaseUrl ? FladderConfig.baseUrl : server); - if (url == null || server.isEmpty) return; - await _fetchServerInfo(url); + if (state.hasBaseUrl) { + await _fetchServerInfo(FladderConfig.baseUrl!); + return; + } + final trimmed = server.trim(); + if (trimmed.isEmpty) return; + if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { + await _fetchServerInfo(trimmed); + } else { + // Try http first, then https + await _fetchServerInfo('http://$trimmed'); + if (state.errorMessage != null) { + await _fetchServerInfo('https://$trimmed'); + } + } } List getSavedAccounts() { diff --git a/lib/screens/login/widgets/advanced_login_options_dialog.dart b/lib/screens/login/widgets/advanced_login_options_dialog.dart index d0a0efb83..2b0231106 100644 --- a/lib/screens/login/widgets/advanced_login_options_dialog.dart +++ b/lib/screens/login/widgets/advanced_login_options_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -85,6 +86,7 @@ class _AdvancedLoginOptionsDialogState extends ConsumerState<_AdvancedLoginOptio } void _save() { - Navigator.of(context).pop(seerrUrlController.text.trim()); + final url = seerrUrlController.text.trim(); + Navigator.of(context).pop(url.isEmpty ? url : normalizeUrl(url)); } } diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 750d9d079..92c9227fd 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/providers/seerr_api_provider.dart'; import 'package:fladder/providers/seerr_dashboard_provider.dart'; import 'package:fladder/providers/seerr_user_provider.dart'; @@ -169,8 +170,8 @@ class _SeerrConnectionDialogState extends ConsumerState { } bool _applyServerUrl({bool showError = true}) { - final serverUrl = serverController.text.trim(); - if (serverUrl.isEmpty) { + final rawUrl = serverController.text.trim(); + if (rawUrl.isEmpty) { if (showError && mounted) { setState(() { error = context.localized.seerrEnterServerUrlFirst; @@ -178,6 +179,10 @@ class _SeerrConnectionDialogState extends ConsumerState { } return false; } + final serverUrl = normalizeUrl(rawUrl); + if (serverUrl != rawUrl) { + serverController.text = serverUrl; + } ref.read(userProvider.notifier).setSeerrServerUrl(serverUrl); return true; } From d35bf09cd44936fcaebd86156c306a0c56856602 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:30:34 +0100 Subject: [PATCH 19/63] fix(login): add QC polling timeout after 5 minutes --- lib/screens/login/login_code_dialog.dart | 8 ++++++++ lib/screens/settings/widgets/seerr_connection_dialog.dart | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/lib/screens/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index 8f0804f07..48588f33c 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -43,7 +43,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() { @@ -59,7 +61,13 @@ class _LoginCodeDialogState extends ConsumerState { void createTimer() { timer?.cancel(); + _pollAttempts = 0; timer = RestartableTimer(const Duration(seconds: 1), () async { + if (_pollAttempts >= _maxPollAttempts) { + timer?.cancel(); + return; + } + _pollAttempts++; try { final result = await ref.read(jellyApiProvider).quickConnectConnectGet( secret: quickConnectInfo.secret, diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 92c9227fd..efec8e6be 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -68,9 +68,11 @@ class _SeerrConnectionDialogState extends ConsumerState { String? error; // QuickConnect state + static const _maxPollAttempts = 150; // ~5 minutes at 2s interval String? _qcCode; String? _qcSecret; RestartableTimer? _qcTimer; + int _qcPollAttempts = 0; @override void initState() { @@ -320,9 +322,15 @@ class _SeerrConnectionDialogState extends ConsumerState { 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; From fec926610660b3ea362d50871f53b4888428d276 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:31:31 +0100 Subject: [PATCH 20/63] fix(security): clear QC secrets in dispose and add security docs --- lib/screens/login/login_screen_credentials.dart | 2 ++ lib/screens/settings/widgets/seerr_connection_dialog.dart | 2 ++ lib/seerr/seerr_chopper_service.dart | 2 ++ 3 files changed, 6 insertions(+) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 2766de792..095d3d409 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -61,6 +61,8 @@ class _LoginScreenCredentialsState extends ConsumerState final hasBaseUrl = ref.watch(authProvider.select((value) => value.hasBaseUrl)); final urlError = ref.watch(authProvider.select((value) => value.errorMessage)); final hasQuickConnect = ref.watch(authProvider.select((value) => value.serverLoginModel?.hasQuickConnect ?? false)); + // Note: hidePasswordLogin is a UI preference, not a security control. + // It hides the password fields but does not disable password-based authentication on the server. final hidePasswordLogin = ref.watch(authProvider.select((value) => value.hidePasswordLogin)) || ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)); diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index efec8e6be..43d531db1 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -93,6 +93,8 @@ class _SeerrConnectionDialogState extends ConsumerState { @override void dispose() { _qcTimer?.cancel(); + _qcSecret = null; + _qcCode = null; apiKeyController.dispose(); serverController.dispose(); localEmailController.dispose(); diff --git a/lib/seerr/seerr_chopper_service.dart b/lib/seerr/seerr_chopper_service.dart index 6d06079d7..a7ac1c901 100644 --- a/lib/seerr/seerr_chopper_service.dart +++ b/lib/seerr/seerr_chopper_service.dart @@ -31,6 +31,8 @@ abstract class SeerrChopperService extends ChopperService { @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, From 9dbd5b81c2fbcdf4a4b0f59cd8cb154e8038010d Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:51:02 +0100 Subject: [PATCH 21/63] fix(deps): upgrade fvp to 0.35.2 to fix nightly mdk-sdk build --- pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 2bfc96249..64c4815d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -889,10 +889,10 @@ packages: dependency: "direct main" description: name: fvp - sha256: "33e34a78d3e4bd3ab87af7279d7bc88ff6025291574edc7e8592abea91e04cb4" + sha256: e03c4ba02c367cde8610c09325d085c9b1efe4f7d98a563950993a1fee17a28b url: "https://pub.dev" source: hosted - version: "0.35.0" + version: "0.35.2" fwfh_cached_network_image: dependency: transitive description: From e4b3b7d1b771b2ade85bb6e98a2c7c9a0f150ea7 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:53:31 +0100 Subject: [PATCH 22/63] fix(ci): allow Docker deploy when create_release is skipped --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7806eaf98..d5474f862 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -717,9 +717,10 @@ jobs: name: Release Web needs: - fetch-info + - build-web - create_release runs-on: ubuntu-latest - if: needs.fetch-info.outputs.build_type == 'release' || needs.fetch-info.outputs.build_type == 'development' + if: always() && !failure() && !cancelled() && (needs.fetch-info.outputs.build_type == 'release' || needs.fetch-info.outputs.build_type == 'development') steps: - name: Checkout repository uses: actions/checkout@v4.1.1 From f5b4428332028d220407bd51df528aff50e5b36f Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:58:07 +0100 Subject: [PATCH 23/63] Revert "fix(deps): upgrade fvp to 0.35.2 to fix nightly mdk-sdk build" This reverts commit 9dbd5b81c2fbcdf4a4b0f59cd8cb154e8038010d. --- pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 64c4815d2..2bfc96249 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -889,10 +889,10 @@ packages: dependency: "direct main" description: name: fvp - sha256: e03c4ba02c367cde8610c09325d085c9b1efe4f7d98a563950993a1fee17a28b + sha256: "33e34a78d3e4bd3ab87af7279d7bc88ff6025291574edc7e8592abea91e04cb4" url: "https://pub.dev" source: hosted - version: "0.35.2" + version: "0.35.0" fwfh_cached_network_image: dependency: transitive description: From ad0cefc64864f1e9ce758759a9571f8de1627b50 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:00:51 +0100 Subject: [PATCH 24/63] fix(ci): pin mdk-sdk to stable release to fix broken nightly builds --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5474f862..4f92a3c43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,9 @@ name: Build Fladder +env: + # Pin mdk-sdk to a stable release to avoid broken nightly builds from SourceForge + FVP_DEPS_URL: https://github.com/wang-bin/mdk-sdk/releases/download/v0.35.1 + on: push: tags: From f9309fb340c43b19645ed2e87b0a6fee05b1d573 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:01:52 +0100 Subject: [PATCH 25/63] fix(ci): pin mdk-sdk to stable release to fix broken nightly builds --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f92a3c43..e3b7ea101 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,5 @@ name: Build Fladder -env: - # Pin mdk-sdk to a stable release to avoid broken nightly builds from SourceForge - FVP_DEPS_URL: https://github.com/wang-bin/mdk-sdk/releases/download/v0.35.1 - on: push: tags: @@ -39,6 +35,8 @@ concurrency: env: NIGHTLY_TAG: nightly + # Pin mdk-sdk to a stable release to avoid broken nightly builds from SourceForge + FVP_DEPS_URL: https://github.com/wang-bin/mdk-sdk/releases/download/v0.35.1 jobs: # Check if workflow should run based on trigger conditions From a4e466704d2d5501d4d808b85c95e2bc163fc203 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:36:33 +0100 Subject: [PATCH 26/63] fix(settings): lock Seerr URL field when SEERR_URL is configured --- lib/screens/settings/widgets/seerr_connection_dialog.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 43d531db1..72778ff33 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -79,7 +79,7 @@ class _SeerrConnectionDialogState extends ConsumerState { super.initState(); final creds = ref.read(userProvider)?.seerrCredentials; apiKeyController = TextEditingController(text: creds?.apiKey ?? ''); - serverController = TextEditingController(text: creds?.serverUrl ?? ''); + serverController = TextEditingController(text: FladderConfig.seerrUrl ?? creds?.serverUrl ?? ''); localEmailController = TextEditingController(); localPasswordController = TextEditingController(); jfUsernameController = TextEditingController(); @@ -174,7 +174,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } bool _applyServerUrl({bool showError = true}) { - final rawUrl = serverController.text.trim(); + final rawUrl = FladderConfig.seerrUrl ?? serverController.text.trim(); if (rawUrl.isEmpty) { if (showError && mounted) { setState(() { @@ -511,6 +511,7 @@ class _SeerrConnectionDialogState extends ConsumerState { controller: serverController, keyboardType: TextInputType.url, textInputAction: TextInputAction.next, + enabled: FladderConfig.seerrUrl == null, onSubmitted: (_) { _applyServerUrl(); _refreshSession(); From aae0ee702db4a3042eaf5a3c6d8ee1b6437f2ef5 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:39:25 +0100 Subject: [PATCH 27/63] fix(login): probe Seerr URL scheme (try https first, then http) --- lib/providers/api_provider.dart | 17 +++++++++ .../advanced_login_options_dialog.dart | 30 +++++++++++++-- .../widgets/seerr_connection_dialog.dart | 38 ++++++++++++++----- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/lib/providers/api_provider.dart b/lib/providers/api_provider.dart index 896ff6751..a159c7e6c 100644 --- a/lib/providers/api_provider.dart +++ b/lib/providers/api_provider.dart @@ -130,6 +130,23 @@ String normalizeUrl(String url) { } } +/// Probes a Seerr server URL by hitting /api/v1/status. +/// Returns the working URL (with scheme) or null. +Future probeSeerrUrl(String baseUrl) async { + final client = HttpClient()..connectionTimeout = const Duration(seconds: 5); + try { + final request = await client.getUrl(Uri.parse('$baseUrl/api/v1/status')); + final response = await request.close(); + if (response.statusCode >= 200 && response.statusCode < 400) { + return baseUrl; + } + } catch (_) { + } finally { + client.close(); + } + return null; +} + Uri? tryParseServerBaseUri(String? url) { if (url == null) return null; final trimmed = url.trim(); diff --git a/lib/screens/login/widgets/advanced_login_options_dialog.dart b/lib/screens/login/widgets/advanced_login_options_dialog.dart index 2b0231106..31286bfad 100644 --- a/lib/screens/login/widgets/advanced_login_options_dialog.dart +++ b/lib/screens/login/widgets/advanced_login_options_dialog.dart @@ -34,6 +34,7 @@ class _AdvancedLoginOptionsDialog extends ConsumerStatefulWidget { class _AdvancedLoginOptionsDialogState extends ConsumerState<_AdvancedLoginOptionsDialog> { late final TextEditingController seerrUrlController = TextEditingController(text: widget.initialSeerrUrl ?? ''); + bool _probing = false; @override void dispose() { @@ -78,15 +79,36 @@ class _AdvancedLoginOptionsDialogState extends ConsumerState<_AdvancedLoginOptio child: Text(context.localized.cancel), ), FilledButton( - onPressed: _save, - child: Text(context.localized.save), + onPressed: _probing ? null : _save, + child: _probing + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) + : Text(context.localized.save), ), ], ); } - void _save() { + Future _save() async { final url = seerrUrlController.text.trim(); - Navigator.of(context).pop(url.isEmpty ? url : normalizeUrl(url)); + if (url.isEmpty) { + Navigator.of(context).pop(url); + return; + } + final hasScheme = url.startsWith('http://') || url.startsWith('https://'); + if (!hasScheme) { + setState(() => _probing = true); + final httpsUrl = normalizeUrl('https://$url'); + final httpUrl = normalizeUrl('http://$url'); + final result = await probeSeerrUrl(httpsUrl) ?? await probeSeerrUrl(httpUrl); + if (!mounted) return; + setState(() => _probing = false); + if (result != null) { + Navigator.of(context).pop(result); + } else { + Navigator.of(context).pop(normalizeUrl(url)); + } + } else { + Navigator.of(context).pop(normalizeUrl(url)); + } } } diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 72778ff33..73754735c 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -173,7 +173,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } } - bool _applyServerUrl({bool showError = true}) { + Future _applyServerUrl({bool showError = true}) async { final rawUrl = FladderConfig.seerrUrl ?? serverController.text.trim(); if (rawUrl.isEmpty) { if (showError && mounted) { @@ -183,7 +183,27 @@ class _SeerrConnectionDialogState extends ConsumerState { } return false; } - final serverUrl = normalizeUrl(rawUrl); + + String serverUrl; + final hasScheme = rawUrl.startsWith('http://') || rawUrl.startsWith('https://'); + if (!hasScheme) { + // Probe https first, then http + final httpsUrl = normalizeUrl('https://$rawUrl'); + final httpUrl = normalizeUrl('http://$rawUrl'); + final result = await probeSeerrUrl(httpsUrl) ?? await probeSeerrUrl(httpUrl); + if (result == null) { + if (showError && mounted) { + setState(() { + error = context.localized.seerrEnterServerUrlFirst; + }); + } + return false; + } + serverUrl = result; + } else { + serverUrl = normalizeUrl(rawUrl); + } + if (serverUrl != rawUrl) { serverController.text = serverUrl; } @@ -192,7 +212,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } Future _useApiKey() async { - if (!_applyServerUrl()) return; + if (!await _applyServerUrl()) return; setState(() { processing = true; error = null; @@ -219,7 +239,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } Future _loginLocal() async { - if (!_applyServerUrl()) return; + if (!await _applyServerUrl()) return; setState(() { processing = true; error = null; @@ -253,7 +273,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } Future _loginJellyfin() async { - if (!_applyServerUrl()) return; + if (!await _applyServerUrl()) return; setState(() { processing = true; error = null; @@ -287,7 +307,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } Future _quickConnectInitiate() async { - if (!_applyServerUrl()) return; + if (!await _applyServerUrl()) return; setState(() { processing = true; error = null; @@ -389,7 +409,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } Future _logout() async { - _applyServerUrl(showError: false); + await _applyServerUrl(showError: false); setState(() { processing = true; error = null; @@ -512,8 +532,8 @@ class _SeerrConnectionDialogState extends ConsumerState { keyboardType: TextInputType.url, textInputAction: TextInputAction.next, enabled: FladderConfig.seerrUrl == null, - onSubmitted: (_) { - _applyServerUrl(); + onSubmitted: (_) async { + await _applyServerUrl(); _refreshSession(); }, ), From 5807cc8078bd2f46d4109ea7267e4d7b795df095 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:42:35 +0100 Subject: [PATCH 28/63] fix(login): try https first for Jellyfin URLs without scheme --- lib/providers/auth_provider.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index a4ce12f51..d4a4df00d 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -205,10 +205,10 @@ class AuthNotifier extends StateNotifier { if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { await _fetchServerInfo(trimmed); } else { - // Try http first, then https - await _fetchServerInfo('http://$trimmed'); + // Try https first, then http + await _fetchServerInfo('https://$trimmed'); if (state.errorMessage != null) { - await _fetchServerInfo('https://$trimmed'); + await _fetchServerInfo('http://$trimmed'); } } } From 33732df94a2f58b05e104f74d949b0f0ca1b5b62 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:26:30 +0100 Subject: [PATCH 29/63] fix(docker): pass SEERR_URL env var into config.json in Dockerfile --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2f5619c71..60554dea6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,10 @@ FROM nginx:alpine EXPOSE 80 ENV BASE_URL="" +ENV SEERR_URL="" COPY build/web /usr/share/nginx/html -RUN echo '{"baseUrl": "${BASE_URL}"}' > /usr/share/nginx/html/assets/config/config.json +RUN echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}"}' > /usr/share/nginx/html/assets/config/config.json -CMD /bin/sh -c 'sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && nginx -g "daemon off;"' +CMD /bin/sh -c 'sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && nginx -g "daemon off;"' From edfd393a03e45396c49d0356b1f28487d50f5752 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:26:55 +0100 Subject: [PATCH 30/63] fix(docker): pass SEERR_URL env var into config.json in Dockerfile-rootless --- Dockerfile-rootless | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index 040d9da3a..8cf8a39dc 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -3,6 +3,7 @@ FROM ghcr.io/nginxinc/nginx-unprivileged:stable-alpine-slim EXPOSE 8080 ENV BASE_URL="" +ENV SEERR_URL="" USER root COPY build/web /usr/share/nginx/html @@ -10,7 +11,7 @@ RUN chown -R nginx:nginx /usr/share/nginx/html USER nginx RUN mkdir -p /usr/share/nginx/html/assets/config && \ - echo '{"baseUrl": "${BASE_URL}"}' > /usr/share/nginx/html/assets/config/config.json + echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}"}' > /usr/share/nginx/html/assets/config/config.json USER root -CMD /bin/sh -c 'sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && nginx -g "daemon off;"' \ No newline at end of file +CMD /bin/sh -c 'sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && nginx -g "daemon off;"' \ No newline at end of file From 98d51d1a0be67801b5a4540daa48f1b342de7c00 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:27:13 +0100 Subject: [PATCH 31/63] docs(docker): add SEERR_URL env var to docker-compose.yml --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 70a3660ce..8c132211c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,3 +5,4 @@ services: - 80:80 environment: - BASE_URL=https://server-url #OPTIONAL: Locks the Fladder front-end to a certain jellyfin server + - SEERR_URL= #OPTIONAL: Pre-fills and locks the Seerr URL in settings From 7ca0df89c5799bd3e6edc7c58e165dea51708539 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:50:32 +0100 Subject: [PATCH 32/63] fix(seerr): handle browser-hidden Set-Cookie header on web with sentinel cookie --- lib/providers/seerr_api_provider.dart | 3 ++- lib/providers/seerr_service_provider.dart | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/providers/seerr_api_provider.dart b/lib/providers/seerr_api_provider.dart index 3c542963f..c1c1ced9f 100644 --- a/lib/providers/seerr_api_provider.dart +++ b/lib/providers/seerr_api_provider.dart @@ -85,7 +85,8 @@ class SeerrRequest implements Interceptor { Map _authHeaders({required String apiKey, required String cookie}) { if (apiKey.isNotEmpty) return {'X-Api-Key': apiKey}; - if (cookie.isNotEmpty) return {'Cookie': cookie}; + if (cookie.isNotEmpty && cookie != kBrowserManagedCookie) return {'Cookie': cookie}; + if (cookie == kBrowserManagedCookie) return const {}; return const {}; } diff --git a/lib/providers/seerr_service_provider.dart b/lib/providers/seerr_service_provider.dart index 9b2e916e8..caebdbbfa 100644 --- a/lib/providers/seerr_service_provider.dart +++ b/lib/providers/seerr_service_provider.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:chopper/chopper.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/images_models.dart'; @@ -11,6 +12,7 @@ import 'package:fladder/seerr/seerr_chopper_service.dart'; import 'package:fladder/seerr/seerr_models.dart'; const tmbdUrl = 'https://image.tmdb.org/t/p/original'; +const kBrowserManagedCookie = '__browser_managed__'; class SeerrService { SeerrService(this.ref, this._api); @@ -716,7 +718,9 @@ class SeerrService { String? _extractSessionCookie(Response response) { final setCookie = response.base.headers['set-cookie']; - if (setCookie == null || setCookie.isEmpty) return null; + if (setCookie == null || setCookie.isEmpty) { + return kIsWeb ? kBrowserManagedCookie : null; + } return setCookie.split(';').first.trim(); } } From fe556597c31dfa37df9460c73eb416831b927ab1 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:21:50 +0100 Subject: [PATCH 33/63] feat(seerr): add default HTTP client factory for Seerr --- lib/util/seerr_http_client.dart | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 lib/util/seerr_http_client.dart diff --git a/lib/util/seerr_http_client.dart b/lib/util/seerr_http_client.dart new file mode 100644 index 000000000..29bf93bc8 --- /dev/null +++ b/lib/util/seerr_http_client.dart @@ -0,0 +1,3 @@ +import 'package:http/http.dart' as http; + +http.Client createSeerrHttpClient() => http.Client(); From 4d6b822d635bd36c624c4a021fe93305792674db Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:22:08 +0100 Subject: [PATCH 34/63] feat(seerr): add web HTTP client factory with withCredentials --- lib/util/seerr_http_client_web.dart | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 lib/util/seerr_http_client_web.dart diff --git a/lib/util/seerr_http_client_web.dart b/lib/util/seerr_http_client_web.dart new file mode 100644 index 000000000..c7c8b9ab0 --- /dev/null +++ b/lib/util/seerr_http_client_web.dart @@ -0,0 +1,6 @@ +import 'package:http/browser_client.dart'; +import 'package:http/http.dart' as http; + +http.Client createSeerrHttpClient() { + return BrowserClient()..withCredentials = true; +} From 8856b60e82bc8b824d075a908973151de1566e2d Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:22:27 +0100 Subject: [PATCH 35/63] feat(seerr): enable withCredentials on web HTTP client for cookie auth --- lib/providers/seerr_api_provider.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/providers/seerr_api_provider.dart b/lib/providers/seerr_api_provider.dart index c1c1ced9f..2a374d5d5 100644 --- a/lib/providers/seerr_api_provider.dart +++ b/lib/providers/seerr_api_provider.dart @@ -10,6 +10,8 @@ import 'package:fladder/providers/seerr_service_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/seerr/seerr_chopper_service.dart'; import 'package:fladder/seerr/seerr_json_converter.dart'; +import 'package:fladder/util/seerr_http_client.dart' + if (dart.library.html) 'package:fladder/util/seerr_http_client_web.dart'; part 'seerr_api_provider.g.dart'; @@ -20,6 +22,7 @@ class SeerrApi extends _$SeerrApi { ref.watch(userProvider.select((u) => u?.seerrCredentials)); final chopperClient = ChopperClient( + client: createSeerrHttpClient(), converter: const SeerrJsonConverter(), interceptors: [ SeerrRequest(ref), From d73318db7ba8bba6b300d6e7df889784c1364977 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:46:43 +0100 Subject: [PATCH 36/63] ci: trigger rebuild From 95ad6f9f754e13e631ce301ed3a290ce3227e7d7 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:22:15 +0100 Subject: [PATCH 37/63] ci: retrigger build From eb925bc39b4ea742f81d43967e319d45c6f671bc Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:09:04 +0100 Subject: [PATCH 38/63] feat(docker): wire HIDE_PASSWORD_LOGIN env var in Dockerfile --- Dockerfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 60554dea6..fb49b5ed6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,15 @@ EXPOSE 80 ENV BASE_URL="" ENV SEERR_URL="" +ENV HIDE_PASSWORD_LOGIN="" COPY build/web /usr/share/nginx/html -RUN echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}"}' > /usr/share/nginx/html/assets/config/config.json +RUN echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}", "hidePasswordLogin": __HIDE_PW__}' > /usr/share/nginx/html/assets/config/config.json -CMD /bin/sh -c 'sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && nginx -g "daemon off;"' +CMD /bin/sh -c '\ + HIDE_PW_VAL=$([ "$HIDE_PASSWORD_LOGIN" = "true" ] && echo true || echo null) && \ + sed -i "s|__HIDE_PW__|${HIDE_PW_VAL}|g" /usr/share/nginx/html/assets/config/config.json && \ + sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ + sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ + nginx -g "daemon off;"' From 57af5d811fd64b80facb0a0a7b13098a352453a6 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:09:38 +0100 Subject: [PATCH 39/63] feat(docker): wire HIDE_PASSWORD_LOGIN env var in Dockerfile-rootless --- Dockerfile-rootless | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index 8cf8a39dc..3717e267b 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -4,6 +4,7 @@ EXPOSE 8080 ENV BASE_URL="" ENV SEERR_URL="" +ENV HIDE_PASSWORD_LOGIN="" USER root COPY build/web /usr/share/nginx/html @@ -11,7 +12,12 @@ RUN chown -R nginx:nginx /usr/share/nginx/html USER nginx RUN mkdir -p /usr/share/nginx/html/assets/config && \ - echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}"}' > /usr/share/nginx/html/assets/config/config.json + echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}", "hidePasswordLogin": __HIDE_PW__}' > /usr/share/nginx/html/assets/config/config.json USER root -CMD /bin/sh -c 'sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && nginx -g "daemon off;"' \ No newline at end of file +CMD /bin/sh -c '\ + HIDE_PW_VAL=$([ "$HIDE_PASSWORD_LOGIN" = "true" ] && echo true || echo null) && \ + sed -i "s|__HIDE_PW__|${HIDE_PW_VAL}|g" /usr/share/nginx/html/assets/config/config.json && \ + sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ + sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ + nginx -g "daemon off;"' \ No newline at end of file From 91a46348045fcb20874c592ca01ed7394e195215 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:09:54 +0100 Subject: [PATCH 40/63] feat(docker): add HIDE_PASSWORD_LOGIN to docker-compose.yml --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 8c132211c..622f6a9eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,3 +6,4 @@ services: environment: - BASE_URL=https://server-url #OPTIONAL: Locks the Fladder front-end to a certain jellyfin server - SEERR_URL= #OPTIONAL: Pre-fills and locks the Seerr URL in settings + - HIDE_PASSWORD_LOGIN= #OPTIONAL: Set to "true" to hide password fields and only show QuickConnect From d32cd6ee2b063ec5ea717a957c698fbfd85f4c48 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:29:54 +0100 Subject: [PATCH 41/63] fix(login): center Back/Refresh row when server field is hidden --- 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 095d3d409..40ea6f325 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -81,7 +81,7 @@ class _LoginScreenCredentialsState extends ConsumerState spacing: 16, children: [ Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: hasBaseUrl ? MainAxisAlignment.center : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, spacing: 8, children: [ From d78abb504eeb1a116a8a837622902a27a970a049 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:30:44 +0100 Subject: [PATCH 42/63] fix(login): hide Advanced button when Seerr URL is set via Docker --- .../login/login_screen_credentials.dart | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 40ea6f325..661f85eb0 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -65,6 +65,7 @@ class _LoginScreenCredentialsState extends ConsumerState // It hides the password fields but does not disable password-based authentication on the server. final hidePasswordLogin = ref.watch(authProvider.select((value) => value.hidePasswordLogin)) || ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)); + final hasSeerrUrl = ref.watch(authProvider.select((value) => value.hasSeerrUrl)); ref.listen( authProvider.select((value) => value.serverLoginModel), @@ -213,32 +214,31 @@ class _LoginScreenCredentialsState extends ConsumerState ), ), ), - IconButton.filledTonal( - onPressed: () async { - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); - final result = await showAdvancedLoginOptionsDialog( - context, - initialSeerrUrl: tempSeerrUrl, - hasSeerrUrl: hasSeerrUrl, - ); - if (result != null) { - ref.read(authProvider.notifier).setTempSeerrUrl(result); - } - }, - icon: const Icon(IconsaxPlusLinear.setting_3), - ), + if (!hasSeerrUrl) + IconButton.filledTonal( + onPressed: () async { + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final result = await showAdvancedLoginOptionsDialog( + context, + initialSeerrUrl: tempSeerrUrl, + hasSeerrUrl: hasSeerrUrl, + ); + if (result != null) { + ref.read(authProvider.notifier).setTempSeerrUrl(result); + } + }, + icon: const Icon(IconsaxPlusLinear.setting_3), + ), ], ), ], - if (hidePasswordLogin) + if (hidePasswordLogin && !hasSeerrUrl) Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton.filledTonal( onPressed: () async { final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); final result = await showAdvancedLoginOptionsDialog( context, initialSeerrUrl: tempSeerrUrl, From ab5a0b6e434d78dabc72bd3fe28493528698d876 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:31:10 +0100 Subject: [PATCH 43/63] fix(login): center Advanced button when password login is hidden --- 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 661f85eb0..89349a92e 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -234,7 +234,7 @@ class _LoginScreenCredentialsState extends ConsumerState ], if (hidePasswordLogin && !hasSeerrUrl) Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton.filledTonal( onPressed: () async { From 0ad22f5f1ad1d450fd79a1c9928d72ea4e68507d Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:14:04 +0100 Subject: [PATCH 44/63] refactor(login): extract shared Advanced button callback into method --- .../login/login_screen_credentials.dart | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 89349a92e..9fc3a04a1 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -216,17 +216,7 @@ class _LoginScreenCredentialsState extends ConsumerState ), if (!hasSeerrUrl) IconButton.filledTonal( - onPressed: () async { - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final result = await showAdvancedLoginOptionsDialog( - context, - initialSeerrUrl: tempSeerrUrl, - hasSeerrUrl: hasSeerrUrl, - ); - if (result != null) { - ref.read(authProvider.notifier).setTempSeerrUrl(result); - } - }, + onPressed: _openAdvancedLoginOptions, icon: const Icon(IconsaxPlusLinear.setting_3), ), ], @@ -237,17 +227,7 @@ class _LoginScreenCredentialsState extends ConsumerState mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton.filledTonal( - onPressed: () async { - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final result = await showAdvancedLoginOptionsDialog( - context, - initialSeerrUrl: tempSeerrUrl, - hasSeerrUrl: hasSeerrUrl, - ); - if (result != null) { - ref.read(authProvider.notifier).setTempSeerrUrl(result); - } - }, + onPressed: _openAdvancedLoginOptions, icon: const Icon(IconsaxPlusLinear.setting_3), ), ], @@ -297,6 +277,19 @@ class _LoginScreenCredentialsState extends ConsumerState ); } + Future _openAdvancedLoginOptions() async { + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); + final result = await showAdvancedLoginOptionsDialog( + context, + initialSeerrUrl: tempSeerrUrl, + hasSeerrUrl: hasSeerrUrl, + ); + if (result != null) { + ref.read(authProvider.notifier).setTempSeerrUrl(result); + } + } + Future Function()? get enterCredentialsTryLogin => emptyFields() ? null : () => loginUsingCredentials(); Future loginUsingCredentials() async { From 61f195c6795043a32f06da9a2786fcbebc19292a Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:29:32 +0100 Subject: [PATCH 45/63] fix(settings): lock hidePasswordLogin toggle when set via Docker --- .../client_settings_advanced.dart | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/screens/settings/client_sections/client_settings_advanced.dart b/lib/screens/settings/client_sections/client_settings_advanced.dart index aed6bf12c..1e59b9567 100644 --- a/lib/screens/settings/client_sections/client_settings_advanced.dart +++ b/lib/screens/settings/client_sections/client_settings_advanced.dart @@ -5,6 +5,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/home_settings_provider.dart'; +import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; @@ -118,14 +119,19 @@ List buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) { SettingsListTile( label: Text(context.localized.hidePasswordLogin), subLabel: Text(context.localized.hidePasswordLoginDescription), - onTap: () => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(hidePasswordLogin: !current.hidePasswordLogin)), + onTap: FladderConfig.hidePasswordLogin != null + ? null + : () => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(hidePasswordLogin: !current.hidePasswordLogin)), trailing: Switch( - value: ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)), - onChanged: (value) => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(hidePasswordLogin: value)), + value: FladderConfig.hidePasswordLogin ?? + ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)), + onChanged: FladderConfig.hidePasswordLogin != null + ? null + : (value) => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(hidePasswordLogin: value)), ), ), ], From d7d578c89bbc827b69c5751e81f0c0a2ad8e8368 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:34:30 +0100 Subject: [PATCH 46/63] feat(docker): add entrypoint script for config and nginx proxy generation --- docker/docker-entrypoint.sh | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100755 docker/docker-entrypoint.sh diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 000000000..0084baf7a --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,50 @@ +#!/bin/sh +set -e + +CONFIG="/usr/share/nginx/html/assets/config/config.json" +NGINX_CONF="/etc/nginx/conf.d/default.conf" +NGINX_TEMPLATE="/docker-nginx.conf.template" + +# --- Build config.json --- + +HIDE_PW_VAL=$([ "$HIDE_PASSWORD_LOGIN" = "true" ] && echo true || echo null) + +# Determine seerrProxyPath: set when both SEERR_URL and SEERR_CUSTOM_HEADERS are provided +if [ -n "$SEERR_URL" ] && [ -n "$SEERR_CUSTOM_HEADERS" ]; then + SEERR_PROXY_PATH="/seerr-proxy" +else + SEERR_PROXY_PATH="" +fi + +SEERR_PROXY_JSON=$([ -n "$SEERR_PROXY_PATH" ] && echo "\"$SEERR_PROXY_PATH\"" || echo null) + +cat > "$CONFIG" < "$NGINX_CONF" + +# --- Start nginx --- + +exec nginx -g "daemon off;" From f19e82fa14af9f8cc1932e42fac1a6a13faffb6e Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:34:45 +0100 Subject: [PATCH 47/63] feat(docker): add nginx config template with proxy placeholder --- docker/nginx.conf.template | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docker/nginx.conf.template diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template new file mode 100644 index 000000000..8866b4370 --- /dev/null +++ b/docker/nginx.conf.template @@ -0,0 +1,11 @@ +server { + listen __PORT__; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + __SEERR_PROXY_BLOCK__ +} From 6a282270a9de8934e94ea9fc9a7d917f926f5f60 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:35:04 +0100 Subject: [PATCH 48/63] refactor(docker): use entrypoint script and add jq for header parsing --- Dockerfile | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index fb49b5ed6..3a70857d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,19 @@ FROM nginx:alpine +RUN apk add --no-cache jq + EXPOSE 80 ENV BASE_URL="" ENV SEERR_URL="" ENV HIDE_PASSWORD_LOGIN="" +ENV SEERR_CUSTOM_HEADERS="" COPY build/web /usr/share/nginx/html +COPY docker/docker-entrypoint.sh /docker-entrypoint.sh +COPY docker/nginx.conf.template /docker-nginx.conf.template -RUN echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}", "hidePasswordLogin": __HIDE_PW__}' > /usr/share/nginx/html/assets/config/config.json +RUN chmod +x /docker-entrypoint.sh && \ + sed -i 's|__PORT__|80|g' /docker-nginx.conf.template -CMD /bin/sh -c '\ - HIDE_PW_VAL=$([ "$HIDE_PASSWORD_LOGIN" = "true" ] && echo true || echo null) && \ - sed -i "s|__HIDE_PW__|${HIDE_PW_VAL}|g" /usr/share/nginx/html/assets/config/config.json && \ - sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ - sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ - nginx -g "daemon off;"' +CMD ["/docker-entrypoint.sh"] From f0af45699bc1cc2030002e0ecbdd79b56882d7e4 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:35:22 +0100 Subject: [PATCH 49/63] refactor(docker): use entrypoint script and add jq for header parsing (rootless) --- Dockerfile-rootless | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index 3717e267b..d68345258 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -5,19 +5,16 @@ EXPOSE 8080 ENV BASE_URL="" ENV SEERR_URL="" ENV HIDE_PASSWORD_LOGIN="" +ENV SEERR_CUSTOM_HEADERS="" USER root +RUN apk add --no-cache jq COPY build/web /usr/share/nginx/html -RUN chown -R nginx:nginx /usr/share/nginx/html +COPY docker/docker-entrypoint.sh /docker-entrypoint.sh +COPY docker/nginx.conf.template /docker-nginx.conf.template -USER nginx -RUN mkdir -p /usr/share/nginx/html/assets/config && \ - echo '{"baseUrl": "${BASE_URL}", "seerrUrl": "${SEERR_URL}", "hidePasswordLogin": __HIDE_PW__}' > /usr/share/nginx/html/assets/config/config.json +RUN chmod +x /docker-entrypoint.sh && \ + chown -R nginx:nginx /usr/share/nginx/html && \ + sed -i 's|__PORT__|8080|g' /docker-nginx.conf.template -USER root -CMD /bin/sh -c '\ - HIDE_PW_VAL=$([ "$HIDE_PASSWORD_LOGIN" = "true" ] && echo true || echo null) && \ - sed -i "s|__HIDE_PW__|${HIDE_PW_VAL}|g" /usr/share/nginx/html/assets/config/config.json && \ - sed -i "s|\${BASE_URL}|${BASE_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ - sed -i "s|\${SEERR_URL}|${SEERR_URL}|g" /usr/share/nginx/html/assets/config/config.json && \ - nginx -g "daemon off;"' \ No newline at end of file +CMD ["/docker-entrypoint.sh"] From c62c2977a00dbd8f9cd9e95a7f605f3b5eef2971 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:35:38 +0100 Subject: [PATCH 50/63] feat(docker): add SEERR_CUSTOM_HEADERS env var --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 622f6a9eb..41013089a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,3 +7,4 @@ services: - BASE_URL=https://server-url #OPTIONAL: Locks the Fladder front-end to a certain jellyfin server - SEERR_URL= #OPTIONAL: Pre-fills and locks the Seerr URL in settings - HIDE_PASSWORD_LOGIN= #OPTIONAL: Set to "true" to hide password fields and only show QuickConnect + - SEERR_CUSTOM_HEADERS= #OPTIONAL: JSON object of headers injected server-side via nginx proxy, e.g. '{"X-Auth": "secret"}' From 914ed3efcb1c367503a22fcb312fa9bb6edc3293 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:35:59 +0100 Subject: [PATCH 51/63] feat(config): add seerrProxyPath field for nginx proxy routing --- lib/util/fladder_config.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/util/fladder_config.dart b/lib/util/fladder_config.dart index 8c47edbf7..d2ffdef45 100644 --- a/lib/util/fladder_config.dart +++ b/lib/util/fladder_config.dart @@ -14,6 +14,10 @@ class FladderConfig { static set hidePasswordLogin(bool? value) => _instance._hidePasswordLogin = value; bool? _hidePasswordLogin; + static String? get seerrProxyPath => _instance._seerrProxyPath; + static set seerrProxyPath(String? value) => _instance._seerrProxyPath = value; + String? _seerrProxyPath; + static void fromJson(Map json) => _instance = FladderConfig._fromJson(json); factory FladderConfig._fromJson(Map json) { @@ -23,6 +27,8 @@ class FladderConfig { final newSeerrUrl = json['seerrUrl'] as String?; config._seerrUrl = newSeerrUrl?.isEmpty == true ? null : newSeerrUrl; config._hidePasswordLogin = json['hidePasswordLogin'] as bool?; + final proxyPath = json['seerrProxyPath'] as String?; + config._seerrProxyPath = proxyPath?.isEmpty == true ? null : proxyPath; return config; } } From 00f580b5b204b94ea0499a232334ff8d51dfe4a4 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:36:11 +0100 Subject: [PATCH 52/63] feat(config): add seerrProxyPath to default config --- config/config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index c08043932..232dc0567 100644 --- a/config/config.json +++ b/config/config.json @@ -1,5 +1,6 @@ { "baseUrl": null, "seerrUrl": null, - "hidePasswordLogin": null + "hidePasswordLogin": null, + "seerrProxyPath": null } From 6421308eb3cdb2f4479cb33ad6fd0ad476f8b096 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:36:24 +0100 Subject: [PATCH 53/63] feat(seerr): route requests through nginx proxy when seerrProxyPath is set --- lib/providers/seerr_api_provider.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/providers/seerr_api_provider.dart b/lib/providers/seerr_api_provider.dart index 2a374d5d5..e610c6e83 100644 --- a/lib/providers/seerr_api_provider.dart +++ b/lib/providers/seerr_api_provider.dart @@ -2,10 +2,12 @@ import 'dart:developer'; import 'dart:io'; import 'package:chopper/chopper.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:fladder/providers/connectivity_provider.dart'; +import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/providers/seerr_service_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/seerr/seerr_chopper_service.dart'; @@ -59,7 +61,14 @@ class SeerrRequest implements Interceptor { final authHeaders = _authHeaders(apiKey: apiKey, cookie: cookie); final customHeaders = creds?.customHeaders ?? {}; final headers = {...authHeaders, ...customHeaders}; - final apiBaseUri = Uri.parse(serverUrl); + + final proxyPath = FladderConfig.seerrProxyPath; + final Uri apiBaseUri; + if (kIsWeb && proxyPath != null) { + apiBaseUri = Uri.base.resolve(proxyPath); + } else { + apiBaseUri = Uri.parse(serverUrl); + } Uri resolvedRequestUri; try { From 93cc7a3961e6d455a75a16d97df35a4ac26077c2 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:18:17 +0100 Subject: [PATCH 54/63] fix(docker): replace sed template with cat heredoc in entrypoint --- docker/docker-entrypoint.sh | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 0084baf7a..05918d8f1 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -3,7 +3,6 @@ set -e CONFIG="/usr/share/nginx/html/assets/config/config.json" NGINX_CONF="/etc/nginx/conf.d/default.conf" -NGINX_TEMPLATE="/docker-nginx.conf.template" # --- Build config.json --- @@ -29,21 +28,32 @@ EOF # --- Build nginx config --- +PROXY_BLOCK="" if [ -n "$SEERR_URL" ] && [ -n "$SEERR_CUSTOM_HEADERS" ]; then # Build proxy_set_header directives from JSON object - HEADER_DIRECTIVES=$(echo "$SEERR_CUSTOM_HEADERS" | jq -r 'to_entries[] | " proxy_set_header \(.key) \"\(.value)\";"') + HEADER_DIRECTIVES=$(echo "$SEERR_CUSTOM_HEADERS" | jq -r 'to_entries[] | " proxy_set_header \(.key) \"\(.value)\";"') - PROXY_BLOCK="location /seerr-proxy/ { - proxy_pass ${SEERR_URL}/; - proxy_set_header Host \$proxy_host; - proxy_ssl_server_name on; + PROXY_BLOCK=" + location /seerr-proxy/ { + proxy_pass ${SEERR_URL}/; + proxy_set_header Host \$proxy_host; + proxy_ssl_server_name on; ${HEADER_DIRECTIVES} - }" -else - PROXY_BLOCK="" + }" fi -sed "s|__SEERR_PROXY_BLOCK__|${PROXY_BLOCK}|g" "$NGINX_TEMPLATE" > "$NGINX_CONF" +cat > "$NGINX_CONF" < Date: Fri, 6 Mar 2026 15:18:43 +0100 Subject: [PATCH 55/63] fix(docker): remove unused nginx.conf.template --- docker/nginx.conf.template | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 docker/nginx.conf.template diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template deleted file mode 100644 index 8866b4370..000000000 --- a/docker/nginx.conf.template +++ /dev/null @@ -1,11 +0,0 @@ -server { - listen __PORT__; - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ /index.html; - } - - __SEERR_PROXY_BLOCK__ -} From 7e9ae0aba0cff0e0ea503c519bc1ffcc17d49428 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:19:20 +0100 Subject: [PATCH 56/63] fix(docker): remove template handling and add PORT env in Dockerfile --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3a70857d6..1af363ed6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,12 +8,11 @@ ENV BASE_URL="" ENV SEERR_URL="" ENV HIDE_PASSWORD_LOGIN="" ENV SEERR_CUSTOM_HEADERS="" +ENV PORT=80 COPY build/web /usr/share/nginx/html COPY docker/docker-entrypoint.sh /docker-entrypoint.sh -COPY docker/nginx.conf.template /docker-nginx.conf.template -RUN chmod +x /docker-entrypoint.sh && \ - sed -i 's|__PORT__|80|g' /docker-nginx.conf.template +RUN chmod +x /docker-entrypoint.sh CMD ["/docker-entrypoint.sh"] From 3fdc9295dfb39f7e542b79cdaa35b3bc755ad907 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:19:44 +0100 Subject: [PATCH 57/63] fix(docker): remove template handling and add PORT env in Dockerfile-rootless --- Dockerfile-rootless | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index d68345258..2541c59ee 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -6,15 +6,14 @@ ENV BASE_URL="" ENV SEERR_URL="" ENV HIDE_PASSWORD_LOGIN="" ENV SEERR_CUSTOM_HEADERS="" +ENV PORT=8080 USER root RUN apk add --no-cache jq COPY build/web /usr/share/nginx/html COPY docker/docker-entrypoint.sh /docker-entrypoint.sh -COPY docker/nginx.conf.template /docker-nginx.conf.template RUN chmod +x /docker-entrypoint.sh && \ - chown -R nginx:nginx /usr/share/nginx/html && \ - sed -i 's|__PORT__|8080|g' /docker-nginx.conf.template + chown -R nginx:nginx /usr/share/nginx/html CMD ["/docker-entrypoint.sh"] From 03359117336310d06c231325ce0943286e216684 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:38:05 +0100 Subject: [PATCH 58/63] title: refactor(docker): use SEERR_PROXY_PATH variable in nginx location block --- docker/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 05918d8f1..7aa823f54 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -34,7 +34,7 @@ if [ -n "$SEERR_URL" ] && [ -n "$SEERR_CUSTOM_HEADERS" ]; then HEADER_DIRECTIVES=$(echo "$SEERR_CUSTOM_HEADERS" | jq -r 'to_entries[] | " proxy_set_header \(.key) \"\(.value)\";"') PROXY_BLOCK=" - location /seerr-proxy/ { + location ${SEERR_PROXY_PATH}/ { proxy_pass ${SEERR_URL}/; proxy_set_header Host \$proxy_host; proxy_ssl_server_name on; From b533317ae00226bd10c75d22cd2cecccfe5763d6 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:39:20 +0100 Subject: [PATCH 59/63] fix(docker): switch back to nginx user before CMD in rootless Dockerfile --- Dockerfile-rootless | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile-rootless b/Dockerfile-rootless index 2541c59ee..c3820400e 100644 --- a/Dockerfile-rootless +++ b/Dockerfile-rootless @@ -14,6 +14,8 @@ COPY build/web /usr/share/nginx/html COPY docker/docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh && \ - chown -R nginx:nginx /usr/share/nginx/html + chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /etc/nginx/conf.d +USER nginx CMD ["/docker-entrypoint.sh"] From ba662348a8fc55595d872436f2b70116d81d2add Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:44:20 +0100 Subject: [PATCH 60/63] chore: reset build.yml and pubspec.lock to develop --- .github/workflows/build.yml | 37 ++++++------------------------------- pubspec.lock | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3b7ea101..ca4037b5d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: - "v*" branches: - master - - feat/seerr-quickconnect + - feat/seerr-docker-env pull_request: paths: - pubspec.yaml @@ -35,8 +35,6 @@ concurrency: env: NIGHTLY_TAG: nightly - # Pin mdk-sdk to a stable release to avoid broken nightly builds from SourceForge - FVP_DEPS_URL: https://github.com/wang-bin/mdk-sdk/releases/download/v0.35.1 jobs: # Check if workflow should run based on trigger conditions @@ -176,44 +174,25 @@ jobs: run: | flutter build apk --debug --build-number=${{github.run_number}} --flavor production - - name: Build Android APK and AAB (signed release) - if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING != '' - env: - ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} + - name: Build Android APK and AAB + if: needs.fetch-info.outputs.build_type != 'development' run: | flutter build apk --release --build-number=${{github.run_number}} --flavor production flutter build appbundle --release --build-number=${{github.run_number}} --flavor production - - name: Build Android APK (fallback debug) - if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING == '' - env: - ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} - run: | - flutter build apk --debug --build-number=${{github.run_number}} --flavor production - - name: Rename APK for PR if: needs.fetch-info.outputs.build_type == 'development' run: | mkdir -p build/app/outputs/android_artifacts mv build/app/outputs/flutter-apk/app-production-debug.apk "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.apk" - - name: Rename APK and AAB (signed release) - if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING != '' - env: - ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} + - name: Rename APK and AAB + if: needs.fetch-info.outputs.build_type != 'development' run: | mkdir -p build/app/outputs/android_artifacts mv build/app/outputs/flutter-apk/app-production-release.apk "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.apk" mv build/app/outputs/bundle/productionRelease/app-production-release.aab "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.aab" - - name: Rename APK (fallback debug) - if: needs.fetch-info.outputs.build_type != 'development' && env.ENCODED_STRING == '' - env: - ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} - run: | - mkdir -p build/app/outputs/android_artifacts - mv build/app/outputs/flutter-apk/app-production-debug.apk "build/app/outputs/android_artifacts/${{ env.ARTIFACT_SUFFIX }}.apk" - - name: Archive Android artifacts uses: actions/upload-artifact@v4.0.0 with: @@ -719,10 +698,9 @@ jobs: name: Release Web needs: - fetch-info - - build-web - create_release runs-on: ubuntu-latest - if: always() && !failure() && !cancelled() && (needs.fetch-info.outputs.build_type == 'release' || needs.fetch-info.outputs.build_type == 'development') + if: needs.fetch-info.outputs.build_type == 'release' steps: - name: Checkout repository uses: actions/checkout@v4.1.1 @@ -757,18 +735,15 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Clean builds folder - if: needs.fetch-info.outputs.build_type == 'release' run: rm -rf build/web - name: Download Artifacts Web - if: needs.fetch-info.outputs.build_type == 'release' uses: actions/download-artifact@v4 with: name: fladder-web-pages path: build/web - name: Deploy to GitHub Pages - if: needs.fetch-info.outputs.build_type == 'release' uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/pubspec.lock b/pubspec.lock index 2bfc96249..840a9ce2e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" charcode: dependency: transitive description: @@ -1249,18 +1249,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" media_kit: dependency: "direct main" description: @@ -1337,10 +1337,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -2118,10 +2118,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.6" timezone: dependency: transitive description: From 304785aaa4637be8cab9fd4079cdb8d7b409eb11 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:00:29 +0100 Subject: [PATCH 61/63] chore: reset build.yml to upstream develop --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca4037b5d..39715280d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,6 @@ on: - "v*" branches: - master - - feat/seerr-docker-env pull_request: paths: - pubspec.yaml From 37444b129d99ab3a8f5a47192164367f88bcfac6 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:53:38 +0100 Subject: [PATCH 62/63] fix: dart formatting issues --- lib/providers/seerr_service_provider.dart | 186 ++++++++++---- lib/screens/login/login_code_dialog.dart | 23 +- .../login/login_screen_credentials.dart | 86 +++++-- .../widgets/seerr_connection_dialog.dart | 62 +++-- lib/seerr/seerr_models.dart | 242 ++++++++++++------ 5 files changed, 420 insertions(+), 179 deletions(-) diff --git a/lib/providers/seerr_service_provider.dart b/lib/providers/seerr_service_provider.dart index caebdbbfa..75476a6e5 100644 --- a/lib/providers/seerr_service_provider.dart +++ b/lib/providers/seerr_service_provider.dart @@ -30,7 +30,8 @@ class SeerrService { final avatar = user.avatar; if (avatar != null && avatar.isNotEmpty) { final serverUrl = ref.read(userProvider)?.seerrCredentials?.serverUrl; - final resolvedAvatar = resolveServerUrl(path: avatar, serverUrl: serverUrl); + final resolvedAvatar = + resolveServerUrl(path: avatar, serverUrl: serverUrl); if (resolvedAvatar != avatar) { return response.copyWith(body: user.copyWith(avatar: resolvedAvatar)); @@ -105,7 +106,8 @@ class SeerrService { return response.body; } - Future> users({int? take, int? skip, String sort = 'displayname'}) async { + Future> users( + {int? take, int? skip, String sort = 'displayname'}) async { final response = await _api.getUsers(take: take, skip: skip, sort: sort); final results = response.body?.results ?? []; final serverUrl = ref.read(userProvider)?.seerrCredentials?.serverUrl; @@ -114,7 +116,8 @@ class SeerrService { final avatar = user.avatar; if (avatar == null || avatar.isEmpty) return user; - final resolvedAvatar = resolveServerUrl(path: avatar, serverUrl: serverUrl); + final resolvedAvatar = + resolveServerUrl(path: avatar, serverUrl: serverUrl); if (resolvedAvatar == avatar) return user; return user.copyWith(avatar: resolvedAvatar); @@ -142,8 +145,10 @@ class SeerrService { String? releaseYear, SeerrRequestStatus? requestStatus, }) { - final keyPrefix = type == SeerrMediaType.movie ? 'tmdb_movie_$tmdbId' : 'tmdb_tv_$tmdbId'; - final id = type == SeerrMediaType.movie ? 'tmdb:movie:$tmdbId' : 'tmdb:tv:$tmdbId'; + final keyPrefix = + type == SeerrMediaType.movie ? 'tmdb_movie_$tmdbId' : 'tmdb_tv_$tmdbId'; + final id = + type == SeerrMediaType.movie ? 'tmdb:movie:$tmdbId' : 'tmdb:tv:$tmdbId'; return SeerrDashboardPosterModel( id: id, @@ -153,8 +158,12 @@ class SeerrService { title: title, overview: overview, images: ImagesData( - primary: posterUrl != null ? ImageData(path: posterUrl, key: '${keyPrefix}_primary') : null, - backDrop: backdropUrl != null ? [ImageData(path: backdropUrl, key: '${keyPrefix}_backdrop')] : null, + primary: posterUrl != null + ? ImageData(path: posterUrl, key: '${keyPrefix}_primary') + : null, + backDrop: backdropUrl != null + ? [ImageData(path: backdropUrl, key: '${keyPrefix}_backdrop')] + : null, ), mediaStatus: mediaStatus ?? SeerrMediaStatus.unknown, requestStatus: requestStatus, @@ -165,11 +174,13 @@ class SeerrService { ); } - Map _seasonStatusMap(List? seasons) { + Map _seasonStatusMap( + List? seasons) { if (seasons == null) return const {}; return { for (final season in seasons) - if (season.seasonNumber != null) season.seasonNumber!: SeerrMediaStatus.fromRaw(season.status), + if (season.seasonNumber != null) + season.seasonNumber!: SeerrMediaStatus.fromRaw(season.status), }; } @@ -232,8 +243,10 @@ class SeerrService { releaseYear: releaseYear, ); } else { - final movieResponse = await movieDetails(tmdbId: tmdbId, language: language); - if (!movieResponse.isSuccessful || movieResponse.body == null) return null; + final movieResponse = + await movieDetails(tmdbId: tmdbId, language: language); + if (!movieResponse.isSuccessful || movieResponse.body == null) + return null; final details = movieResponse.body!; String? releaseYear; final releaseDate = details.releaseDate; @@ -258,11 +271,13 @@ class SeerrService { return null; } - Future> movieDetails({required int tmdbId, String? language}) { + Future> movieDetails( + {required int tmdbId, String? language}) { return _api.getMovieDetails(tmdbId, language: language); } - Future> tvDetails({required int tvId, String? language}) { + Future> tvDetails( + {required int tvId, String? language}) { return _api.getTvDetails(tvId, language: language); } @@ -306,10 +321,15 @@ class SeerrService { ); } - Future> discoverTrending({int? page, String? language}) async { - final response = await _api.getDiscoverTrending(page: page, language: language); + Future> discoverTrending( + {int? page, String? language}) async { + final response = + await _api.getDiscoverTrending(page: page, language: language); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } SeerrMediaType? _resolveMediaType(SeerrDiscoverItem item) { @@ -340,7 +360,8 @@ class SeerrService { : (item.title ?? item.originalTitle ?? item.name ?? ''); String? releaseYear; - final dateString = type == SeerrMediaType.tvshow ? item.firstAirDate : item.releaseDate; + final dateString = + type == SeerrMediaType.tvshow ? item.firstAirDate : item.releaseDate; if (dateString != null && dateString.isNotEmpty) { releaseYear = dateString.split('-').first; } @@ -353,42 +374,64 @@ class SeerrService { overview: item.overview ?? '', posterUrl: item.posterUrl, backdropUrl: item.backdropUrl, - mediaStatus: item.mediaInfo?.status != null ? SeerrMediaStatus.fromRaw(item.mediaInfo?.status) : null, + mediaStatus: item.mediaInfo?.status != null + ? SeerrMediaStatus.fromRaw(item.mediaInfo?.status) + : null, mediaInfo: item.mediaInfo, releaseYear: releaseYear, ); } - Future> discoverPopularMovies({int? page, String? language}) async { + Future> discoverPopularMovies( + {int? page, String? language}) async { final response = await _api.getDiscoverMovies( page: page, language: language, - sortBy: SeerrSortBy.popularityDesc.valueForMode(SeerrSearchMode.discoverMovies), + sortBy: SeerrSortBy.popularityDesc + .valueForMode(SeerrSearchMode.discoverMovies), ); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } - Future> discoverPopularSeries({int? page, String? language}) async { + Future> discoverPopularSeries( + {int? page, String? language}) async { final response = await _api.getDiscoverTv( page: page, language: language, - sortBy: SeerrSortBy.popularityDesc.valueForMode(SeerrSearchMode.discoverTv), + sortBy: + SeerrSortBy.popularityDesc.valueForMode(SeerrSearchMode.discoverTv), ); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } - Future> discoverExpectedMovies({int? page, String? language}) async { - final response = await _api.getDiscoverMoviesUpcoming(page: page, language: language); + Future> discoverExpectedMovies( + {int? page, String? language}) async { + final response = + await _api.getDiscoverMoviesUpcoming(page: page, language: language); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } - Future> discoverExpectedSeries({int? page, String? language}) async { - final response = await _api.getDiscoverTvUpcoming(page: page, language: language); + Future> discoverExpectedSeries( + {int? page, String? language}) async { + final response = + await _api.getDiscoverTvUpcoming(page: page, language: language); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } Future> discoverRelatedMovies({ @@ -397,7 +440,10 @@ class SeerrService { }) async { final response = await _api.getMovieSimilar(tmdbId, language: language); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } Future> discoverRelatedSeries({ @@ -406,25 +452,36 @@ class SeerrService { }) async { final response = await _api.getTvSimilar(tmdbId, language: language); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } Future> discoverRecommendedMovies({ required int tmdbId, String? language, }) async { - final response = await _api.getMovieRecommendations(tmdbId, language: language); + final response = + await _api.getMovieRecommendations(tmdbId, language: language); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } Future> discoverRecommendedSeries({ required int tmdbId, String? language, }) async { - final response = await _api.getTvRecommendations(tmdbId, language: language); + final response = + await _api.getTvRecommendations(tmdbId, language: language); final results = response.body?.results ?? const []; - return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); + return results + .map(_posterFromDiscoverItem) + .whereType() + .toList(growable: false); } Future> myRequests({ @@ -511,7 +568,8 @@ class SeerrService { return _api.deleteMedia(mediaId); } - Future> deleteMediaFile({required int mediaId, bool? is4k}) { + Future> deleteMediaFile( + {required int mediaId, bool? is4k}) { return _api.deleteMediaFile(mediaId, is4k: is4k); } @@ -523,10 +581,12 @@ class SeerrService { return _api.updateMediaStatus(mediaId, status, body: body); } - Future> searchPosters({required String query, int? page, String? language}) async { + Future> searchPosters( + {required String query, int? page, String? language}) async { if (query.trim().isEmpty) return const []; - final response = await _api.search(query: query, page: page, language: language); + final response = + await _api.search(query: query, page: page, language: language); final results = response.body?.results ?? const []; final items = []; @@ -543,21 +603,27 @@ class SeerrService { Future>> getTvGenres() => _api.getTvGenres(); - Future>> getMovieWatchProviders({String? watchRegion}) { + Future>> getMovieWatchProviders( + {String? watchRegion}) { return _api.getMovieWatchProviders(watchRegion: watchRegion); } - Future>> getTvWatchProviders({String? watchRegion}) { + Future>> getTvWatchProviders( + {String? watchRegion}) { return _api.getTvWatchProviders(watchRegion: watchRegion); } - Future>> getWatchProviderRegions() => _api.getWatchProviderRegions(); + Future>> getWatchProviderRegions() => + _api.getWatchProviderRegions(); - Future> getMovieCertifications() => _api.getMovieCertifications(); + Future> getMovieCertifications() => + _api.getMovieCertifications(); - Future> getTvCertifications() => _api.getTvCertifications(); + Future> getTvCertifications() => + _api.getTvCertifications(); - Future> discoverTrendingPaged({int? page, String? language}) => + Future> discoverTrendingPaged( + {int? page, String? language}) => _api.getDiscoverTrending(page: page, language: language); // Helper method for discover search @@ -632,15 +698,20 @@ class SeerrService { }) => _api.searchCompany(query: query, page: page); - SeerrDashboardPosterModel? posterFromDiscoverItem(SeerrDiscoverItem item) => _posterFromDiscoverItem(item); + SeerrDashboardPosterModel? posterFromDiscoverItem(SeerrDiscoverItem item) => + _posterFromDiscoverItem(item); - Future authenticateLocal({required String email, required String password, Map? headers}) async { + Future authenticateLocal( + {required String email, + required String password, + Map? headers}) async { final response = await _api.authenticateLocal( SeerrAuthLocalBody(email: email, password: password), headers: headers, ); if (!response.isSuccessful) { - throw HttpException('Local authentication failed (${response.statusCode})'); + throw HttpException( + 'Local authentication failed (${response.statusCode})'); } final cookie = _extractSessionCookie(response); if (cookie == null || cookie.isEmpty) { @@ -649,8 +720,12 @@ class SeerrService { return cookie; } - Future authenticateJellyfin({required String username, required String password, Map? headers}) async { - final response = await _authenticateJellyfin(username: username, password: password, headers: headers); + Future authenticateJellyfin( + {required String username, + required String password, + Map? headers}) async { + final response = await _authenticateJellyfin( + username: username, password: password, headers: headers); return _requireSessionCookie(response, label: 'Jellyfin'); } @@ -675,7 +750,10 @@ class SeerrService { Future logout() async => await _api.logout(); - Future> _authenticateJellyfin({required String username, required String password, Map? headers}) async { + Future> _authenticateJellyfin( + {required String username, + required String password, + Map? headers}) async { var response = await _api.authenticateJellyfin( SeerrAuthJellyfinBody(username: username, password: password), headers: headers, @@ -704,10 +782,12 @@ class SeerrService { detailsString.contains('required')); } - String _requireSessionCookie(Response response, {required String label}) { + String _requireSessionCookie(Response response, + {required String label}) { if (!response.isSuccessful) { final details = response.error ?? response.body; - throw HttpException('$label authentication failed (${response.statusCode})\n$details'); + throw HttpException( + '$label authentication failed (${response.statusCode})\n$details'); } final cookie = _extractSessionCookie(response); if (cookie == null || cookie.isEmpty) { diff --git a/lib/screens/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index 48588f33c..76487fee9 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -126,12 +126,14 @@ class _LoginCodeDialogState extends ConsumerState { padding: const EdgeInsets.all(12.0), child: Text( code, - style: - Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - wordSpacing: 8, - letterSpacing: 8, - ), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + wordSpacing: 8, + letterSpacing: 8, + ), textAlign: TextAlign.center, semanticsLabel: code, ), @@ -142,9 +144,14 @@ class _LoginCodeDialogState extends ConsumerState { TextButton.icon( onPressed: () async { final baseUrl = FladderConfig.baseUrl ?? - ref.read(authProvider).serverLoginModel?.tempCredentials.url; + ref + .read(authProvider) + .serverLoginModel + ?.tempCredentials + .url; if (baseUrl != null && baseUrl.isNotEmpty) { - await ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); + await ext.launchUrl( + context, '$baseUrl/web/#/quickconnect'); timer?.reset(); } }, diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 9fc3a04a1..4ba2198d6 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -30,11 +30,14 @@ class LoginScreenCredentials extends ConsumerStatefulWidget { const LoginScreenCredentials({super.key}); @override - ConsumerState createState() => _LoginScreenCredentialsState(); + ConsumerState createState() => + _LoginScreenCredentialsState(); } -class _LoginScreenCredentialsState extends ConsumerState { - late final TextEditingController serverTextController = TextEditingController(text: ''); +class _LoginScreenCredentialsState + extends ConsumerState { + late final TextEditingController serverTextController = + TextEditingController(text: ''); final usernameController = TextEditingController(); final passwordController = TextEditingController(); final FocusNode focusNode = FocusNode(); @@ -52,20 +55,28 @@ class _LoginScreenCredentialsState extends ConsumerState @override Widget build(BuildContext context) { - final existingUsers = ref.watch(authProvider.select((value) => value.accounts)); + final existingUsers = + ref.watch(authProvider.select((value) => value.accounts)); final otherCredentials = existingUsers.map((e) => e.credentials).toList(); - final serverCredentials = ref.watch(authProvider.select((value) => value.serverLoginModel)); + final serverCredentials = + ref.watch(authProvider.select((value) => value.serverLoginModel)); final users = serverCredentials?.accounts ?? []; final provider = ref.read(authProvider.notifier); final loading = ref.watch(authProvider.select((value) => value.loading)); - final hasBaseUrl = ref.watch(authProvider.select((value) => value.hasBaseUrl)); - final urlError = ref.watch(authProvider.select((value) => value.errorMessage)); - final hasQuickConnect = ref.watch(authProvider.select((value) => value.serverLoginModel?.hasQuickConnect ?? false)); + final hasBaseUrl = + ref.watch(authProvider.select((value) => value.hasBaseUrl)); + final urlError = + ref.watch(authProvider.select((value) => value.errorMessage)); + final hasQuickConnect = ref.watch(authProvider + .select((value) => value.serverLoginModel?.hasQuickConnect ?? false)); // Note: hidePasswordLogin is a UI preference, not a security control. // It hides the password fields but does not disable password-based authentication on the server. - final hidePasswordLogin = ref.watch(authProvider.select((value) => value.hidePasswordLogin)) || - ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)); - final hasSeerrUrl = ref.watch(authProvider.select((value) => value.hasSeerrUrl)); + final hidePasswordLogin = ref + .watch(authProvider.select((value) => value.hidePasswordLogin)) || + ref.watch( + clientSettingsProvider.select((value) => value.hidePasswordLogin)); + final hasSeerrUrl = + ref.watch(authProvider.select((value) => value.hasSeerrUrl)); ref.listen( authProvider.select((value) => value.serverLoginModel), @@ -82,7 +93,8 @@ class _LoginScreenCredentialsState extends ConsumerState spacing: 16, children: [ Row( - mainAxisAlignment: hasBaseUrl ? MainAxisAlignment.center : MainAxisAlignment.start, + mainAxisAlignment: + hasBaseUrl ? MainAxisAlignment.center : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, spacing: 8, children: [ @@ -135,7 +147,8 @@ class _LoginScreenCredentialsState extends ConsumerState AnimatedFadeSize( duration: const Duration(milliseconds: 250), child: loading - ? CircularProgressIndicator(key: UniqueKey(), strokeCap: StrokeCap.round) + ? CircularProgressIndicator( + key: UniqueKey(), strokeCap: StrokeCap.round) : LoginUserGrid( users: users, onPressed: (value) { @@ -177,7 +190,8 @@ class _LoginScreenCredentialsState extends ConsumerState focusNode: focusNode, autocorrect: false, textInputAction: TextInputAction.send, - onSubmitted: (value) => enterCredentialsTryLogin?.call(), + onSubmitted: (value) => + enterCredentialsTryLogin?.call(), onChanged: (value) => setState(() {}), label: context.localized.password, ), @@ -202,7 +216,10 @@ class _LoginScreenCredentialsState extends ConsumerState width: 18, height: 18, child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.inversePrimary, strokeCap: StrokeCap.round), + color: Theme.of(context) + .colorScheme + .inversePrimary, + strokeCap: StrokeCap.round), ) : Row( mainAxisSize: MainAxisSize.min, @@ -235,7 +252,9 @@ class _LoginScreenCredentialsState extends ConsumerState if (hasQuickConnect) FilledButton( onPressed: () async { - final result = await ref.read(jellyApiProvider).quickConnectInitiate(); + final result = await ref + .read(jellyApiProvider) + .quickConnectInitiate(); if (result.body != null) { await openLoginCodeDialog( context, @@ -248,7 +267,9 @@ class _LoginScreenCredentialsState extends ConsumerState }, ); } else { - FladderSnack.show(context.localized.quickConnectPostFailed, context: context); + FladderSnack.show( + context.localized.quickConnectPostFailed, + context: context); } }, child: Row( @@ -278,8 +299,10 @@ class _LoginScreenCredentialsState extends ConsumerState } Future _openAdvancedLoginOptions() async { - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); + final tempSeerrUrl = + ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final hasSeerrUrl = + ref.read(authProvider.select((value) => value.hasSeerrUrl)); final result = await showAdvancedLoginOptionsDialog( context, initialSeerrUrl: tempSeerrUrl, @@ -290,7 +313,8 @@ class _LoginScreenCredentialsState extends ConsumerState } } - Future Function()? get enterCredentialsTryLogin => emptyFields() ? null : () => loginUsingCredentials(); + Future Function()? get enterCredentialsTryLogin => + emptyFields() ? null : () => loginUsingCredentials(); Future loginUsingCredentials() async { setState(() { @@ -319,7 +343,8 @@ class _LoginScreenCredentialsState extends ConsumerState return; } - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final tempSeerrUrl = + ref.read(authProvider.select((value) => value.tempSeerrUrl)); if (tempSeerrUrl != null && tempSeerrUrl.isNotEmpty) { await _tryAuthenticateSeerr(tempSeerrUrl); } @@ -333,7 +358,8 @@ class _LoginScreenCredentialsState extends ConsumerState try { ref.read(userProvider.notifier).setSeerrServerUrl(seerrUrl); - final tempCookie = ref.read(authProvider.select((value) => value.tempSeerrSessionCookie)); + final tempCookie = ref + .read(authProvider.select((value) => value.tempSeerrSessionCookie)); if (tempCookie != null) { ref.read(userProvider.notifier).setSeerrSessionCookie(tempCookie); ref.read(userProvider.notifier).setSeerrApiKey(''); @@ -375,7 +401,8 @@ class _LoginScreenCredentialsState extends ConsumerState ref.read(authProvider.notifier).authenticateUsingSecret(secret), ); if (response.isSuccess && context.mounted) { - final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final tempSeerrUrl = + ref.read(authProvider.select((value) => value.tempSeerrUrl)); if (tempSeerrUrl != null && tempSeerrUrl.isNotEmpty) { await _tryAuthenticateSeerr(tempSeerrUrl); } @@ -398,17 +425,21 @@ Future loggedInGoToHome(BuildContext context, WidgetRef ref) async { } } -Future _handleLogin(BuildContext context, AccountModel user, WidgetRef ref) async { +Future _handleLogin( + BuildContext context, AccountModel user, WidgetRef ref) async { await ref.read(authProvider.notifier).switchUser(); await ref.read(sharedUtilityProvider).updateAccountInfo(user.copyWith( lastUsed: DateTime.now(), )); - ref.read(userProvider.notifier).updateUser(user.copyWith(lastUsed: DateTime.now())); + ref + .read(userProvider.notifier) + .updateUser(user.copyWith(lastUsed: DateTime.now())); loggedInGoToHome(context, ref); } -void tapLoggedInAccount(BuildContext context, AccountModel user, WidgetRef ref) async { +void tapLoggedInAccount( + BuildContext context, AccountModel user, WidgetRef ref) async { Future loginFunction() => _handleLogin(context, user, ref); switch (user.authMethod) { case Authentication.autoLogin: @@ -426,7 +457,8 @@ void tapLoggedInAccount(BuildContext context, AccountModel user, WidgetRef ref) if (newPin == user.localPin) { loginFunction(); } else { - FladderSnack.show(context.localized.incorrectPinTryAgain, context: context); + FladderSnack.show(context.localized.incorrectPinTryAgain, + context: context); } }); } diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 73754735c..72e7beccd 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -48,7 +48,8 @@ class SeerrConnectionDialog extends ConsumerStatefulWidget { const SeerrConnectionDialog({super.key}); @override - ConsumerState createState() => _SeerrConnectionDialogState(); + ConsumerState createState() => + _SeerrConnectionDialogState(); } class _SeerrConnectionDialogState extends ConsumerState { @@ -79,7 +80,8 @@ class _SeerrConnectionDialogState extends ConsumerState { super.initState(); final creds = ref.read(userProvider)?.seerrCredentials; apiKeyController = TextEditingController(text: creds?.apiKey ?? ''); - serverController = TextEditingController(text: FladderConfig.seerrUrl ?? creds?.serverUrl ?? ''); + serverController = TextEditingController( + text: FladderConfig.seerrUrl ?? creds?.serverUrl ?? ''); localEmailController = TextEditingController(); localPasswordController = TextEditingController(); jfUsernameController = TextEditingController(); @@ -185,12 +187,14 @@ class _SeerrConnectionDialogState extends ConsumerState { } String serverUrl; - final hasScheme = rawUrl.startsWith('http://') || rawUrl.startsWith('https://'); + final hasScheme = + rawUrl.startsWith('http://') || rawUrl.startsWith('https://'); if (!hasScheme) { // Probe https first, then http final httpsUrl = normalizeUrl('https://$rawUrl'); final httpUrl = normalizeUrl('http://$rawUrl'); - final result = await probeSeerrUrl(httpsUrl) ?? await probeSeerrUrl(httpUrl); + final result = + await probeSeerrUrl(httpsUrl) ?? await probeSeerrUrl(httpUrl); if (result == null) { if (showError && mounted) { setState(() { @@ -354,7 +358,8 @@ class _SeerrConnectionDialogState extends ConsumerState { } _qcPollAttempts++; try { - final authenticated = await ref.read(seerrApiProvider).quickConnectCheck(secret); + final authenticated = + await ref.read(seerrApiProvider).quickConnectCheck(secret); if (!mounted) return; if (authenticated) { await _quickConnectAuthenticate(secret); @@ -375,7 +380,8 @@ class _SeerrConnectionDialogState extends ConsumerState { }); try { - final cookie = await ref.read(seerrApiProvider).quickConnectAuthenticate(secret); + final cookie = + await ref.read(seerrApiProvider).quickConnectAuthenticate(secret); if (!mounted) return; if (cookie == null || cookie.isEmpty) { setState(() { @@ -460,7 +466,8 @@ class _SeerrConnectionDialogState extends ConsumerState { ), child: Row( children: [ - Icon(IconsaxPlusLinear.warning_2, color: Theme.of(context).colorScheme.onErrorContainer), + Icon(IconsaxPlusLinear.warning_2, + color: Theme.of(context).colorScheme.onErrorContainer), const SizedBox(width: 8), Expanded( child: Text( @@ -477,8 +484,10 @@ class _SeerrConnectionDialogState extends ConsumerState { Widget _loggedInContent() { final serverUrl = ref.read(userProvider)?.seerrCredentials?.serverUrl ?? ''; - final displayName = - seerrUser?.displayName ?? seerrUser?.username ?? seerrUser?.email ?? context.localized.seerrUnknownUser; + final displayName = seerrUser?.displayName ?? + seerrUser?.username ?? + seerrUser?.email ?? + context.localized.seerrUnknownUser; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -496,7 +505,8 @@ class _SeerrConnectionDialogState extends ConsumerState { spacing: 8, children: [ seerrUser?.avatar != null && seerrUser!.avatar!.isNotEmpty - ? CircleAvatar(backgroundImage: NetworkImage(seerrUser!.avatar!)) + ? CircleAvatar( + backgroundImage: NetworkImage(seerrUser!.avatar!)) : CircleAvatar(child: Icon(FladderItemType.person.icon)), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -569,7 +579,9 @@ class _SeerrConnectionDialogState extends ConsumerState { ), ), const SizedBox(width: 8), - IconButton(onPressed: _addHeader, icon: const Icon(IconsaxPlusBold.add_circle)), + IconButton( + onPressed: _addHeader, + icon: const Icon(IconsaxPlusBold.add_circle)), ], ), const SizedBox(height: 8), @@ -635,7 +647,10 @@ class _SeerrConnectionDialogState extends ConsumerState { FilledButton( onPressed: processing ? null : _useApiKey, child: processing - ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator()) : Text(context.localized.save), ), ], @@ -674,7 +689,10 @@ class _SeerrConnectionDialogState extends ConsumerState { FilledButton( onPressed: processing ? null : _loginLocal, child: processing - ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator()) : Text(context.localized.login), ), ], @@ -712,7 +730,10 @@ class _SeerrConnectionDialogState extends ConsumerState { FilledButton( onPressed: processing ? null : _loginJellyfin, child: processing - ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator()) : Text(context.localized.login), ), ], @@ -769,8 +790,13 @@ class _SeerrConnectionDialogState extends ConsumerState { 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), + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator()) + : Text(_qcCode != null + ? context.localized.refresh + : context.localized.quickConnectTitle), ), ), ], @@ -800,7 +826,9 @@ class _SeerrConnectionDialogState extends ConsumerState { child: CircularProgressIndicator(strokeCap: StrokeCap.round), ) else - AnimatedFadeSize(child: seerrUser != null ? _loggedInContent() : _authContent()), + AnimatedFadeSize( + child: + seerrUser != null ? _loggedInContent() : _authContent()), ], ), ), diff --git a/lib/seerr/seerr_models.dart b/lib/seerr/seerr_models.dart index 65c990b66..d52377dfe 100644 --- a/lib/seerr/seerr_models.dart +++ b/lib/seerr/seerr_models.dart @@ -127,13 +127,34 @@ enum SeerrSortBy { static const Map _sortValues = { SeerrSortBy.popularityAsc: (movie: 'popularity.asc', tv: 'popularity.asc'), - SeerrSortBy.popularityDesc: (movie: 'popularity.desc', tv: 'popularity.desc'), - SeerrSortBy.releaseDateAsc: (movie: 'primary_release_date.asc', tv: 'first_air_date.asc'), - SeerrSortBy.releaseDateDesc: (movie: 'primary_release_date.desc', tv: 'first_air_date.desc'), - SeerrSortBy.voteAverageAsc: (movie: 'vote_average.asc', tv: 'vote_average.asc'), - SeerrSortBy.voteAverageDesc: (movie: 'vote_average.desc', tv: 'vote_average.desc'), - SeerrSortBy.titleAsc: (movie: 'original_title.asc', tv: 'original_name.asc'), - SeerrSortBy.titleDesc: (movie: 'original_title.desc', tv: 'original_name.desc'), + SeerrSortBy.popularityDesc: ( + movie: 'popularity.desc', + tv: 'popularity.desc' + ), + SeerrSortBy.releaseDateAsc: ( + movie: 'primary_release_date.asc', + tv: 'first_air_date.asc' + ), + SeerrSortBy.releaseDateDesc: ( + movie: 'primary_release_date.desc', + tv: 'first_air_date.desc' + ), + SeerrSortBy.voteAverageAsc: ( + movie: 'vote_average.asc', + tv: 'vote_average.asc' + ), + SeerrSortBy.voteAverageDesc: ( + movie: 'vote_average.desc', + tv: 'vote_average.desc' + ), + SeerrSortBy.titleAsc: ( + movie: 'original_title.asc', + tv: 'original_name.asc' + ), + SeerrSortBy.titleDesc: ( + movie: 'original_title.desc', + tv: 'original_name.desc' + ), }; /// Map to TMDB sort values; some keys differ for movies vs TV. @@ -178,7 +199,8 @@ class SeerrStatus { this.commitsBehind, }); - factory SeerrStatus.fromJson(Map json) => _$SeerrStatusFromJson(json); + factory SeerrStatus.fromJson(Map json) => + _$SeerrStatusFromJson(json); Map toJson() => _$SeerrStatusToJson(this); } @@ -200,7 +222,8 @@ abstract class SeerrUserModel with _$SeerrUserModel { int? tvQuotaDays, }) = _SeerrUserModel; - factory SeerrUserModel.fromJson(Map json) => _$SeerrUserModelFromJson(json); + factory SeerrUserModel.fromJson(Map json) => + _$SeerrUserModelFromJson(json); } @JsonSerializable() @@ -210,7 +233,8 @@ class SeerrUserQuota { SeerrUserQuota({this.movie, this.tv}); - factory SeerrUserQuota.fromJson(Map json) => _$SeerrUserQuotaFromJson(json); + factory SeerrUserQuota.fromJson(Map json) => + _$SeerrUserQuotaFromJson(json); Map toJson() => _$SeerrUserQuotaToJson(this); } @@ -224,9 +248,11 @@ class SeerrQuotaEntry { bool get hasRestrictions => restricted == true || limit != 0; - SeerrQuotaEntry({this.days, this.limit, this.used, this.remaining, this.restricted}); + SeerrQuotaEntry( + {this.days, this.limit, this.used, this.remaining, this.restricted}); - factory SeerrQuotaEntry.fromJson(Map json) => _$SeerrQuotaEntryFromJson(json); + factory SeerrQuotaEntry.fromJson(Map json) => + _$SeerrQuotaEntryFromJson(json); Map toJson() => _$SeerrQuotaEntryToJson(this); } @@ -275,13 +301,16 @@ extension SeerrUserLabelExtension on SeerrUserModel { extension SeerrUserPermissions on SeerrUserModel { int get _permissionValue => permissions ?? 0; - bool get isAdmin => (_permissionValue & SeerrPermission.admin.bit) == SeerrPermission.admin.bit; + bool get isAdmin => + (_permissionValue & SeerrPermission.admin.bit) == + SeerrPermission.admin.bit; bool hasPermission(SeerrPermission permission) => isAdmin ? true : (_permissionValue & permission.bit) == permission.bit; bool get canManageRequests => - hasPermission(SeerrPermission.manageRequests) || hasPermission(SeerrPermission.requestAdvanced); + hasPermission(SeerrPermission.manageRequests) || + hasPermission(SeerrPermission.requestAdvanced); bool get canManageUsers => hasPermission(SeerrPermission.manageUsers); @@ -306,7 +335,8 @@ class SeerrUserSettings { this.originalLanguage, }); - factory SeerrUserSettings.fromJson(Map json) => _$SeerrUserSettingsFromJson(json); + factory SeerrUserSettings.fromJson(Map json) => + _$SeerrUserSettingsFromJson(json); Map toJson() => _$SeerrUserSettingsToJson(this); } @@ -320,7 +350,8 @@ class SeerrUsersResponse { this.pageInfo, }); - factory SeerrUsersResponse.fromJson(Map json) => _$SeerrUsersResponseFromJson(json); + factory SeerrUsersResponse.fromJson(Map json) => + _$SeerrUsersResponseFromJson(json); Map toJson() => _$SeerrUsersResponseToJson(this); } @@ -352,7 +383,9 @@ abstract class SeerrServer { } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) -abstract class SeerrSonarrServer with _$SeerrSonarrServer implements SeerrServer { +abstract class SeerrSonarrServer + with _$SeerrSonarrServer + implements SeerrServer { const factory SeerrSonarrServer({ int? id, String? name, @@ -380,7 +413,8 @@ abstract class SeerrSonarrServer with _$SeerrSonarrServer implements SeerrServer List? activeTags, }) = _SeerrSonarrServer; - factory SeerrSonarrServer.fromJson(Map json) => _$SeerrSonarrServerFromJson(json); + factory SeerrSonarrServer.fromJson(Map json) => + _$SeerrSonarrServerFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -392,11 +426,14 @@ abstract class SeerrSonarrServerResponse with _$SeerrSonarrServerResponse { List? tags, }) = _SeerrSonarrServerResponse; - factory SeerrSonarrServerResponse.fromJson(Map json) => _$SeerrSonarrServerResponseFromJson(json); + factory SeerrSonarrServerResponse.fromJson(Map json) => + _$SeerrSonarrServerResponseFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) -abstract class SeerrRadarrServer with _$SeerrRadarrServer implements SeerrServer { +abstract class SeerrRadarrServer + with _$SeerrRadarrServer + implements SeerrServer { const factory SeerrRadarrServer({ int? id, String? name, @@ -424,7 +461,8 @@ abstract class SeerrRadarrServer with _$SeerrRadarrServer implements SeerrServer List? activeTags, }) = _SeerrRadarrServer; - factory SeerrRadarrServer.fromJson(Map json) => _$SeerrRadarrServerFromJson(json); + factory SeerrRadarrServer.fromJson(Map json) => + _$SeerrRadarrServerFromJson(json); } extension SeerrSonarrServerDefaults on SeerrSonarrServer { @@ -444,7 +482,8 @@ abstract class SeerrRadarrServerResponse with _$SeerrRadarrServerResponse { List? tags, }) = _SeerrRadarrServerResponse; - factory SeerrRadarrServerResponse.fromJson(Map json) => _$SeerrRadarrServerResponseFromJson(json); + factory SeerrRadarrServerResponse.fromJson(Map json) => + _$SeerrRadarrServerResponseFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -454,7 +493,8 @@ abstract class SeerrServiceProfile with _$SeerrServiceProfile { String? name, }) = _SeerrServiceProfile; - factory SeerrServiceProfile.fromJson(Map json) => _$SeerrServiceProfileFromJson(json); + factory SeerrServiceProfile.fromJson(Map json) => + _$SeerrServiceProfileFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -464,7 +504,8 @@ abstract class SeerrServiceTag with _$SeerrServiceTag { String? label, }) = _SeerrServiceTag; - factory SeerrServiceTag.fromJson(Map json) => _$SeerrServiceTagFromJson(json); + factory SeerrServiceTag.fromJson(Map json) => + _$SeerrServiceTagFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -475,7 +516,8 @@ abstract class SeerrRootFolder with _$SeerrRootFolder { String? path, }) = _SeerrRootFolder; - factory SeerrRootFolder.fromJson(Map json) => _$SeerrRootFolderFromJson(json); + factory SeerrRootFolder.fromJson(Map json) => + _$SeerrRootFolderFromJson(json); } @JsonSerializable() @@ -491,7 +533,8 @@ class SeerrContentRating { this.descriptors, }); - factory SeerrContentRating.fromJson(Map json) => _$SeerrContentRatingFromJson(json); + factory SeerrContentRating.fromJson(Map json) => + _$SeerrContentRatingFromJson(json); Map toJson() => _$SeerrContentRatingToJson(this); } @@ -505,7 +548,8 @@ class SeerrCredits { this.crew, }); - factory SeerrCredits.fromJson(Map json) => _$SeerrCreditsFromJson(json); + factory SeerrCredits.fromJson(Map json) => + _$SeerrCreditsFromJson(json); Map toJson() => _$SeerrCreditsToJson(this); } @@ -532,7 +576,8 @@ class SeerrCast { this.internalProfilePath, }); - factory SeerrCast.fromJson(Map json) => _$SeerrCastFromJson(json); + factory SeerrCast.fromJson(Map json) => + _$SeerrCastFromJson(json); Map toJson() => _$SeerrCastToJson(this); } @@ -557,7 +602,8 @@ class SeerrCrew { this.internalProfilePath, }); - factory SeerrCrew.fromJson(Map json) => _$SeerrCrewFromJson(json); + factory SeerrCrew.fromJson(Map json) => + _$SeerrCrewFromJson(json); Map toJson() => _$SeerrCrewToJson(this); } @@ -603,7 +649,8 @@ class SeerrMovieDetails { this.contentRatings, }); - factory SeerrMovieDetails.fromJson(Map json) => _$SeerrMovieDetailsFromJson(json); + factory SeerrMovieDetails.fromJson(Map json) => + _$SeerrMovieDetailsFromJson(json); Map toJson() => _$SeerrMovieDetailsToJson(this); } @@ -657,7 +704,8 @@ class SeerrTvDetails { this.contentRatings, }); - factory SeerrTvDetails.fromJson(Map json) => _$SeerrTvDetailsFromJson(json); + factory SeerrTvDetails.fromJson(Map json) => + _$SeerrTvDetailsFromJson(json); Map toJson() => _$SeerrTvDetailsToJson(this); } @@ -671,12 +719,14 @@ class SeerrGenre { this.name, }); - factory SeerrGenre.fromJson(Map json) => _$SeerrGenreFromJson(json); + factory SeerrGenre.fromJson(Map json) => + _$SeerrGenreFromJson(json); Map toJson() => _$SeerrGenreToJson(this); @override bool operator ==(Object other) => - identical(this, other) || other is SeerrGenre && runtimeType == other.runtimeType && id == other.id; + identical(this, other) || + other is SeerrGenre && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; @@ -692,7 +742,8 @@ class SeerrKeyword { this.name, }); - factory SeerrKeyword.fromJson(Map json) => _$SeerrKeywordFromJson(json); + factory SeerrKeyword.fromJson(Map json) => + _$SeerrKeywordFromJson(json); Map toJson() => _$SeerrKeywordToJson(this); } @@ -718,7 +769,8 @@ class SeerrSeason { this.mediaId, }); - factory SeerrSeason.fromJson(Map json) => _$SeerrSeasonFromJson(json); + factory SeerrSeason.fromJson(Map json) => + _$SeerrSeasonFromJson(json); Map toJson() => _$SeerrSeasonToJson(this); } @@ -741,7 +793,8 @@ class SeerrSeasonDetails { this.episodes, }); - factory SeerrSeasonDetails.fromJson(Map json) => _$SeerrSeasonDetailsFromJson(json); + factory SeerrSeasonDetails.fromJson(Map json) => + _$SeerrSeasonDetailsFromJson(json); Map toJson() => _$SeerrSeasonDetailsToJson(this); } @@ -770,7 +823,8 @@ class SeerrEpisode { this.voteCount, }); - factory SeerrEpisode.fromJson(Map json) => _$SeerrEpisodeFromJson(json); + factory SeerrEpisode.fromJson(Map json) => + _$SeerrEpisodeFromJson(json); Map toJson() => _$SeerrEpisodeToJson(this); } @@ -866,7 +920,8 @@ class SeerrDownloadStatusEpisode { this.id, }); - factory SeerrDownloadStatusEpisode.fromJson(Map json) => _$SeerrDownloadStatusEpisodeFromJson(json); + factory SeerrDownloadStatusEpisode.fromJson(Map json) => + _$SeerrDownloadStatusEpisodeFromJson(json); Map toJson() => _$SeerrDownloadStatusEpisodeToJson(this); } @@ -902,7 +957,8 @@ class SeerrDownloadStatus { return ((size! - (sizeLeft ?? 0)) / size!) * 100; } - factory SeerrDownloadStatus.fromJson(Map json) => _$SeerrDownloadStatusFromJson(json); + factory SeerrDownloadStatus.fromJson(Map json) => + _$SeerrDownloadStatusFromJson(json); Map toJson() => _$SeerrDownloadStatusToJson(this); } @@ -926,9 +982,11 @@ abstract class SeerrMediaInfo with _$SeerrMediaInfo { String? get primaryJellyfinMediaId => jellyfinMediaId4k ?? jellyfinMediaId; - SeerrMediaStatus? get mediaStatus => status != null ? SeerrMediaStatus.fromRaw(status) : null; + SeerrMediaStatus? get mediaStatus => + status != null ? SeerrMediaStatus.fromRaw(status) : null; - factory SeerrMediaInfo.fromJson(Map json) => _$SeerrMediaInfoFromJson(json); + factory SeerrMediaInfo.fromJson(Map json) => + _$SeerrMediaInfoFromJson(json); } @JsonSerializable() @@ -947,7 +1005,8 @@ class SeerrMediaInfoSeason { this.updatedAt, }); - factory SeerrMediaInfoSeason.fromJson(Map json) => _$SeerrMediaInfoSeasonFromJson(json); + factory SeerrMediaInfoSeason.fromJson(Map json) => + _$SeerrMediaInfoSeasonFromJson(json); Map toJson() => _$SeerrMediaInfoSeasonToJson(this); } @@ -965,7 +1024,8 @@ class SeerrExternalIds { this.twitterId, }); - factory SeerrExternalIds.fromJson(Map json) => _$SeerrExternalIdsFromJson(json); + factory SeerrExternalIds.fromJson(Map json) => + _$SeerrExternalIdsFromJson(json); Map toJson() => _$SeerrExternalIdsToJson(this); } @@ -979,7 +1039,8 @@ class SeerrRatingsResponse { this.imdb, }); - factory SeerrRatingsResponse.fromJson(Map json) => _$SeerrRatingsResponseFromJson(json); + factory SeerrRatingsResponse.fromJson(Map json) => + _$SeerrRatingsResponseFromJson(json); Map toJson() => _$SeerrRatingsResponseToJson(this); } @@ -1003,7 +1064,8 @@ class SeerrRtRating { this.url, }); - factory SeerrRtRating.fromJson(Map json) => _$SeerrRtRatingFromJson(json); + factory SeerrRtRating.fromJson(Map json) => + _$SeerrRtRatingFromJson(json); Map toJson() => _$SeerrRtRatingToJson(this); } @@ -1019,7 +1081,8 @@ class SeerrImdbRating { this.criticsScore, }); - factory SeerrImdbRating.fromJson(Map json) => _$SeerrImdbRatingFromJson(json); + factory SeerrImdbRating.fromJson(Map json) => + _$SeerrImdbRatingFromJson(json); Map toJson() => _$SeerrImdbRatingToJson(this); } @@ -1033,7 +1096,8 @@ class SeerrRequestsResponse { this.pageInfo, }); - factory SeerrRequestsResponse.fromJson(Map json) => _$SeerrRequestsResponseFromJson(json); + factory SeerrRequestsResponse.fromJson(Map json) => + _$SeerrRequestsResponseFromJson(json); Map toJson() => _$SeerrRequestsResponseToJson(this); } @@ -1070,7 +1134,8 @@ class SeerrMediaRequest { SeerrRequestStatus get requestStatus => SeerrRequestStatus.fromRaw(status); - factory SeerrMediaRequest.fromJson(Map json) => _$SeerrMediaRequestFromJson(json); + factory SeerrMediaRequest.fromJson(Map json) => + _$SeerrMediaRequestFromJson(json); Map toJson() => _$SeerrMediaRequestToJson(this); } @@ -1082,7 +1147,8 @@ List? _parseRequestSeasons(List? seasons) { .map((season) { if (season is num) return season.toInt(); if (season is Map) { - final value = season['seasonNumber'] ?? season['season'] ?? season['id']; + final value = + season['seasonNumber'] ?? season['season'] ?? season['id']; if (value is num) return value.toInt(); if (value is String) return int.tryParse(value); } @@ -1106,7 +1172,8 @@ class SeerrPageInfo { this.page, }); - factory SeerrPageInfo.fromJson(Map json) => _$SeerrPageInfoFromJson(json); + factory SeerrPageInfo.fromJson(Map json) => + _$SeerrPageInfoFromJson(json); Map toJson() => _$SeerrPageInfoToJson(this); } @@ -1135,7 +1202,8 @@ class SeerrCreateRequestBody { this.userId, }); - factory SeerrCreateRequestBody.fromJson(Map json) => _$SeerrCreateRequestBodyFromJson(json); + factory SeerrCreateRequestBody.fromJson(Map json) => + _$SeerrCreateRequestBodyFromJson(json); Map toJson() => _$SeerrCreateRequestBodyToJson(this); } @@ -1157,7 +1225,8 @@ class SeerrMedia { this.requests, }); - factory SeerrMedia.fromJson(Map json) => _$SeerrMediaFromJson(json); + factory SeerrMedia.fromJson(Map json) => + _$SeerrMediaFromJson(json); Map toJson() => _$SeerrMediaToJson(this); } @@ -1171,7 +1240,8 @@ class SeerrMediaResponse { this.pageInfo, }); - factory SeerrMediaResponse.fromJson(Map json) => _$SeerrMediaResponseFromJson(json); + factory SeerrMediaResponse.fromJson(Map json) => + _$SeerrMediaResponseFromJson(json); Map toJson() => _$SeerrMediaResponseToJson(this); } @@ -1185,7 +1255,8 @@ enum SeerrMediaType { const SeerrMediaType(); - static SeerrMediaType fromString(String mediaType) => switch (mediaType.toLowerCase()) { + static SeerrMediaType fromString(String mediaType) => + switch (mediaType.toLowerCase()) { 'movie' => SeerrMediaType.movie, 'tvshow' || 'tv' => SeerrMediaType.tvshow, 'person' => SeerrMediaType.person, @@ -1228,7 +1299,8 @@ class SeerrDiscoverItem { this.mediaId, }); - factory SeerrDiscoverItem.fromJson(Map json) => _$SeerrDiscoverItemFromJson(json); + factory SeerrDiscoverItem.fromJson(Map json) => + _$SeerrDiscoverItemFromJson(json); Map toJson() => _$SeerrDiscoverItemToJson(this); } @@ -1247,7 +1319,8 @@ class SeerrDiscoverResponse { this.totalResults, }); - factory SeerrDiscoverResponse.fromJson(Map json) => _$SeerrDiscoverResponseFromJson(json); + factory SeerrDiscoverResponse.fromJson(Map json) => + _$SeerrDiscoverResponseFromJson(json); Map toJson() => _$SeerrDiscoverResponseToJson(this); } @@ -1257,7 +1330,8 @@ class SeerrGenreResponse { SeerrGenreResponse({this.genres}); - factory SeerrGenreResponse.fromJson(Map json) => _$SeerrGenreResponseFromJson(json); + factory SeerrGenreResponse.fromJson(Map json) => + _$SeerrGenreResponseFromJson(json); Map toJson() => _$SeerrGenreResponseToJson(this); } @@ -1279,13 +1353,16 @@ class SeerrWatchProvider { this.displayPriority, }); - factory SeerrWatchProvider.fromJson(Map json) => _$SeerrWatchProviderFromJson(json); + factory SeerrWatchProvider.fromJson(Map json) => + _$SeerrWatchProviderFromJson(json); Map toJson() => _$SeerrWatchProviderToJson(this); @override bool operator ==(Object other) => identical(this, other) || - other is SeerrWatchProvider && runtimeType == other.runtimeType && providerId == other.providerId; + other is SeerrWatchProvider && + runtimeType == other.runtimeType && + providerId == other.providerId; @override int get hashCode => providerId.hashCode; @@ -1302,13 +1379,16 @@ class SeerrWatchProviderRegion { SeerrWatchProviderRegion({this.iso31661, this.englishName, this.nativeName}); - factory SeerrWatchProviderRegion.fromJson(Map json) => _$SeerrWatchProviderRegionFromJson(json); + factory SeerrWatchProviderRegion.fromJson(Map json) => + _$SeerrWatchProviderRegionFromJson(json); Map toJson() => _$SeerrWatchProviderRegionToJson(this); @override bool operator ==(Object other) => identical(this, other) || - other is SeerrWatchProviderRegion && runtimeType == other.runtimeType && iso31661 == other.iso31661; + other is SeerrWatchProviderRegion && + runtimeType == other.runtimeType && + iso31661 == other.iso31661; @override int get hashCode => iso31661.hashCode; @@ -1326,13 +1406,16 @@ class SeerrCertification { this.order, }); - factory SeerrCertification.fromJson(Map json) => _$SeerrCertificationFromJson(json); + factory SeerrCertification.fromJson(Map json) => + _$SeerrCertificationFromJson(json); Map toJson() => _$SeerrCertificationToJson(this); @override bool operator ==(Object other) => identical(this, other) || - other is SeerrCertification && runtimeType == other.runtimeType && certification == other.certification; + other is SeerrCertification && + runtimeType == other.runtimeType && + certification == other.certification; @override int get hashCode => certification.hashCode; @@ -1395,7 +1478,8 @@ class SeerrAuthLocalBody { required this.password, }); - factory SeerrAuthLocalBody.fromJson(Map json) => _$SeerrAuthLocalBodyFromJson(json); + factory SeerrAuthLocalBody.fromJson(Map json) => + _$SeerrAuthLocalBodyFromJson(json); Map toJson() => _$SeerrAuthLocalBodyToJson(this); } @@ -1414,7 +1498,8 @@ class SeerrAuthJellyfinBody { this.hostname, }); - factory SeerrAuthJellyfinBody.fromJson(Map json) => _$SeerrAuthJellyfinBodyFromJson(json); + factory SeerrAuthJellyfinBody.fromJson(Map json) => + _$SeerrAuthJellyfinBodyFromJson(json); Map toJson() => _$SeerrAuthJellyfinBodyToJson(this); } @@ -1491,7 +1576,10 @@ class SeerrCompany { @override bool operator ==(Object other) => - identical(this, other) || other is SeerrCompany && runtimeType == other.runtimeType && id == other.id; + identical(this, other) || + other is SeerrCompany && + runtimeType == other.runtimeType && + id == other.id; @override int get hashCode => id.hashCode; @@ -1516,9 +1604,10 @@ class SeerrSearchCompanyResponse { factory SeerrSearchCompanyResponse.fromJson(Map json) { return SeerrSearchCompanyResponse( page: json['page'] as int?, - results: - (json['results'] as List?)?.map((e) => SeerrCompany.fromJson(e as Map)).toList() ?? - [], + results: (json['results'] as List?) + ?.map((e) => SeerrCompany.fromJson(e as Map)) + .toList() ?? + [], totalPages: json['total_pages'] as int?, totalResults: json['total_results'] as int?, ); @@ -1539,21 +1628,24 @@ extension SeerrMovieDetailsExtension on SeerrMovieDetails { } String? get backdropUrl { - if (internalBackdropPath == null || internalBackdropPath!.isEmpty) return null; + if (internalBackdropPath == null || internalBackdropPath!.isEmpty) + return null; return '$_tmdbImageBaseUrl$internalBackdropPath'; } } extension SeerrCastExtension on SeerrCast { String? get profileUrl { - if (internalProfilePath == null || internalProfilePath!.isEmpty) return null; + if (internalProfilePath == null || internalProfilePath!.isEmpty) + return null; return '$_tmdbProfileBaseUrl$internalProfilePath'; } } extension SeerrCrewExtension on SeerrCrew { String? get profileUrl { - if (internalProfilePath == null || internalProfilePath!.isEmpty) return null; + if (internalProfilePath == null || internalProfilePath!.isEmpty) + return null; return '$_tmdbProfileBaseUrl$internalProfilePath'; } } @@ -1565,7 +1657,8 @@ extension SeerrTvDetailsExtension on SeerrTvDetails { } String? get backdropUrl { - if (internalBackdropPath == null || internalBackdropPath!.isEmpty) return null; + if (internalBackdropPath == null || internalBackdropPath!.isEmpty) + return null; return '$_tmdbImageBaseUrl$internalBackdropPath'; } } @@ -1591,7 +1684,8 @@ extension SeerrDiscoverItemExtension on SeerrDiscoverItem { } String? get backdropUrl { - if (internalBackdropPath == null || internalBackdropPath!.isEmpty) return null; + if (internalBackdropPath == null || internalBackdropPath!.isEmpty) + return null; return '$_tmdbImageBaseUrl$internalBackdropPath'; } } From 66f4c650d28d4d85daf6cd77510e36ae78abe888 Mon Sep 17 00:00:00 2001 From: v3DJG6GL <72495210+v3DJG6GL@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:01:07 +0100 Subject: [PATCH 63/63] fix: add curly braces to if-statements, format with line-length 120 --- lib/providers/seerr_service_provider.dart | 183 ++++--------- lib/screens/login/login_code_dialog.dart | 26 +- .../login/login_screen_credentials.dart | 85 ++---- .../widgets/seerr_connection_dialog.dart | 65 ++--- lib/seerr/seerr_chopper_service.chopper.dart | 78 ++---- lib/seerr/seerr_models.dart | 245 ++++++------------ 6 files changed, 209 insertions(+), 473 deletions(-) diff --git a/lib/providers/seerr_service_provider.dart b/lib/providers/seerr_service_provider.dart index 75476a6e5..511e4a844 100644 --- a/lib/providers/seerr_service_provider.dart +++ b/lib/providers/seerr_service_provider.dart @@ -30,8 +30,7 @@ class SeerrService { final avatar = user.avatar; if (avatar != null && avatar.isNotEmpty) { final serverUrl = ref.read(userProvider)?.seerrCredentials?.serverUrl; - final resolvedAvatar = - resolveServerUrl(path: avatar, serverUrl: serverUrl); + final resolvedAvatar = resolveServerUrl(path: avatar, serverUrl: serverUrl); if (resolvedAvatar != avatar) { return response.copyWith(body: user.copyWith(avatar: resolvedAvatar)); @@ -106,8 +105,7 @@ class SeerrService { return response.body; } - Future> users( - {int? take, int? skip, String sort = 'displayname'}) async { + Future> users({int? take, int? skip, String sort = 'displayname'}) async { final response = await _api.getUsers(take: take, skip: skip, sort: sort); final results = response.body?.results ?? []; final serverUrl = ref.read(userProvider)?.seerrCredentials?.serverUrl; @@ -116,8 +114,7 @@ class SeerrService { final avatar = user.avatar; if (avatar == null || avatar.isEmpty) return user; - final resolvedAvatar = - resolveServerUrl(path: avatar, serverUrl: serverUrl); + final resolvedAvatar = resolveServerUrl(path: avatar, serverUrl: serverUrl); if (resolvedAvatar == avatar) return user; return user.copyWith(avatar: resolvedAvatar); @@ -145,10 +142,8 @@ class SeerrService { String? releaseYear, SeerrRequestStatus? requestStatus, }) { - final keyPrefix = - type == SeerrMediaType.movie ? 'tmdb_movie_$tmdbId' : 'tmdb_tv_$tmdbId'; - final id = - type == SeerrMediaType.movie ? 'tmdb:movie:$tmdbId' : 'tmdb:tv:$tmdbId'; + final keyPrefix = type == SeerrMediaType.movie ? 'tmdb_movie_$tmdbId' : 'tmdb_tv_$tmdbId'; + final id = type == SeerrMediaType.movie ? 'tmdb:movie:$tmdbId' : 'tmdb:tv:$tmdbId'; return SeerrDashboardPosterModel( id: id, @@ -158,12 +153,8 @@ class SeerrService { title: title, overview: overview, images: ImagesData( - primary: posterUrl != null - ? ImageData(path: posterUrl, key: '${keyPrefix}_primary') - : null, - backDrop: backdropUrl != null - ? [ImageData(path: backdropUrl, key: '${keyPrefix}_backdrop')] - : null, + primary: posterUrl != null ? ImageData(path: posterUrl, key: '${keyPrefix}_primary') : null, + backDrop: backdropUrl != null ? [ImageData(path: backdropUrl, key: '${keyPrefix}_backdrop')] : null, ), mediaStatus: mediaStatus ?? SeerrMediaStatus.unknown, requestStatus: requestStatus, @@ -174,13 +165,11 @@ class SeerrService { ); } - Map _seasonStatusMap( - List? seasons) { + Map _seasonStatusMap(List? seasons) { if (seasons == null) return const {}; return { for (final season in seasons) - if (season.seasonNumber != null) - season.seasonNumber!: SeerrMediaStatus.fromRaw(season.status), + if (season.seasonNumber != null) season.seasonNumber!: SeerrMediaStatus.fromRaw(season.status), }; } @@ -243,10 +232,10 @@ class SeerrService { releaseYear: releaseYear, ); } else { - final movieResponse = - await movieDetails(tmdbId: tmdbId, language: language); - if (!movieResponse.isSuccessful || movieResponse.body == null) + final movieResponse = await movieDetails(tmdbId: tmdbId, language: language); + if (!movieResponse.isSuccessful || movieResponse.body == null) { return null; + } final details = movieResponse.body!; String? releaseYear; final releaseDate = details.releaseDate; @@ -271,13 +260,11 @@ class SeerrService { return null; } - Future> movieDetails( - {required int tmdbId, String? language}) { + Future> movieDetails({required int tmdbId, String? language}) { return _api.getMovieDetails(tmdbId, language: language); } - Future> tvDetails( - {required int tvId, String? language}) { + Future> tvDetails({required int tvId, String? language}) { return _api.getTvDetails(tvId, language: language); } @@ -321,15 +308,10 @@ class SeerrService { ); } - Future> discoverTrending( - {int? page, String? language}) async { - final response = - await _api.getDiscoverTrending(page: page, language: language); + Future> discoverTrending({int? page, String? language}) async { + final response = await _api.getDiscoverTrending(page: page, language: language); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } SeerrMediaType? _resolveMediaType(SeerrDiscoverItem item) { @@ -360,8 +342,7 @@ class SeerrService { : (item.title ?? item.originalTitle ?? item.name ?? ''); String? releaseYear; - final dateString = - type == SeerrMediaType.tvshow ? item.firstAirDate : item.releaseDate; + final dateString = type == SeerrMediaType.tvshow ? item.firstAirDate : item.releaseDate; if (dateString != null && dateString.isNotEmpty) { releaseYear = dateString.split('-').first; } @@ -374,64 +355,42 @@ class SeerrService { overview: item.overview ?? '', posterUrl: item.posterUrl, backdropUrl: item.backdropUrl, - mediaStatus: item.mediaInfo?.status != null - ? SeerrMediaStatus.fromRaw(item.mediaInfo?.status) - : null, + mediaStatus: item.mediaInfo?.status != null ? SeerrMediaStatus.fromRaw(item.mediaInfo?.status) : null, mediaInfo: item.mediaInfo, releaseYear: releaseYear, ); } - Future> discoverPopularMovies( - {int? page, String? language}) async { + Future> discoverPopularMovies({int? page, String? language}) async { final response = await _api.getDiscoverMovies( page: page, language: language, - sortBy: SeerrSortBy.popularityDesc - .valueForMode(SeerrSearchMode.discoverMovies), + sortBy: SeerrSortBy.popularityDesc.valueForMode(SeerrSearchMode.discoverMovies), ); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } - Future> discoverPopularSeries( - {int? page, String? language}) async { + Future> discoverPopularSeries({int? page, String? language}) async { final response = await _api.getDiscoverTv( page: page, language: language, - sortBy: - SeerrSortBy.popularityDesc.valueForMode(SeerrSearchMode.discoverTv), + sortBy: SeerrSortBy.popularityDesc.valueForMode(SeerrSearchMode.discoverTv), ); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } - Future> discoverExpectedMovies( - {int? page, String? language}) async { - final response = - await _api.getDiscoverMoviesUpcoming(page: page, language: language); + Future> discoverExpectedMovies({int? page, String? language}) async { + final response = await _api.getDiscoverMoviesUpcoming(page: page, language: language); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } - Future> discoverExpectedSeries( - {int? page, String? language}) async { - final response = - await _api.getDiscoverTvUpcoming(page: page, language: language); + Future> discoverExpectedSeries({int? page, String? language}) async { + final response = await _api.getDiscoverTvUpcoming(page: page, language: language); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } Future> discoverRelatedMovies({ @@ -440,10 +399,7 @@ class SeerrService { }) async { final response = await _api.getMovieSimilar(tmdbId, language: language); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } Future> discoverRelatedSeries({ @@ -452,36 +408,25 @@ class SeerrService { }) async { final response = await _api.getTvSimilar(tmdbId, language: language); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } Future> discoverRecommendedMovies({ required int tmdbId, String? language, }) async { - final response = - await _api.getMovieRecommendations(tmdbId, language: language); + final response = await _api.getMovieRecommendations(tmdbId, language: language); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } Future> discoverRecommendedSeries({ required int tmdbId, String? language, }) async { - final response = - await _api.getTvRecommendations(tmdbId, language: language); + final response = await _api.getTvRecommendations(tmdbId, language: language); final results = response.body?.results ?? const []; - return results - .map(_posterFromDiscoverItem) - .whereType() - .toList(growable: false); + return results.map(_posterFromDiscoverItem).whereType().toList(growable: false); } Future> myRequests({ @@ -568,8 +513,7 @@ class SeerrService { return _api.deleteMedia(mediaId); } - Future> deleteMediaFile( - {required int mediaId, bool? is4k}) { + Future> deleteMediaFile({required int mediaId, bool? is4k}) { return _api.deleteMediaFile(mediaId, is4k: is4k); } @@ -581,12 +525,10 @@ class SeerrService { return _api.updateMediaStatus(mediaId, status, body: body); } - Future> searchPosters( - {required String query, int? page, String? language}) async { + Future> searchPosters({required String query, int? page, String? language}) async { if (query.trim().isEmpty) return const []; - final response = - await _api.search(query: query, page: page, language: language); + final response = await _api.search(query: query, page: page, language: language); final results = response.body?.results ?? const []; final items = []; @@ -603,27 +545,21 @@ class SeerrService { Future>> getTvGenres() => _api.getTvGenres(); - Future>> getMovieWatchProviders( - {String? watchRegion}) { + Future>> getMovieWatchProviders({String? watchRegion}) { return _api.getMovieWatchProviders(watchRegion: watchRegion); } - Future>> getTvWatchProviders( - {String? watchRegion}) { + Future>> getTvWatchProviders({String? watchRegion}) { return _api.getTvWatchProviders(watchRegion: watchRegion); } - Future>> getWatchProviderRegions() => - _api.getWatchProviderRegions(); + Future>> getWatchProviderRegions() => _api.getWatchProviderRegions(); - Future> getMovieCertifications() => - _api.getMovieCertifications(); + Future> getMovieCertifications() => _api.getMovieCertifications(); - Future> getTvCertifications() => - _api.getTvCertifications(); + Future> getTvCertifications() => _api.getTvCertifications(); - Future> discoverTrendingPaged( - {int? page, String? language}) => + Future> discoverTrendingPaged({int? page, String? language}) => _api.getDiscoverTrending(page: page, language: language); // Helper method for discover search @@ -698,20 +634,16 @@ class SeerrService { }) => _api.searchCompany(query: query, page: page); - SeerrDashboardPosterModel? posterFromDiscoverItem(SeerrDiscoverItem item) => - _posterFromDiscoverItem(item); + SeerrDashboardPosterModel? posterFromDiscoverItem(SeerrDiscoverItem item) => _posterFromDiscoverItem(item); Future authenticateLocal( - {required String email, - required String password, - Map? headers}) async { + {required String email, required String password, Map? headers}) async { final response = await _api.authenticateLocal( SeerrAuthLocalBody(email: email, password: password), headers: headers, ); if (!response.isSuccessful) { - throw HttpException( - 'Local authentication failed (${response.statusCode})'); + throw HttpException('Local authentication failed (${response.statusCode})'); } final cookie = _extractSessionCookie(response); if (cookie == null || cookie.isEmpty) { @@ -721,11 +653,8 @@ class SeerrService { } Future authenticateJellyfin( - {required String username, - required String password, - Map? headers}) async { - final response = await _authenticateJellyfin( - username: username, password: password, headers: headers); + {required String username, required String password, Map? headers}) async { + final response = await _authenticateJellyfin(username: username, password: password, headers: headers); return _requireSessionCookie(response, label: 'Jellyfin'); } @@ -751,9 +680,7 @@ class SeerrService { Future logout() async => await _api.logout(); Future> _authenticateJellyfin( - {required String username, - required String password, - Map? headers}) async { + {required String username, required String password, Map? headers}) async { var response = await _api.authenticateJellyfin( SeerrAuthJellyfinBody(username: username, password: password), headers: headers, @@ -782,12 +709,10 @@ class SeerrService { detailsString.contains('required')); } - String _requireSessionCookie(Response response, - {required String label}) { + String _requireSessionCookie(Response response, {required String label}) { if (!response.isSuccessful) { final details = response.error ?? response.body; - throw HttpException( - '$label authentication failed (${response.statusCode})\n$details'); + throw HttpException('$label authentication failed (${response.statusCode})\n$details'); } final cookie = _extractSessionCookie(response); if (cookie == null || cookie.isEmpty) { diff --git a/lib/screens/login/login_code_dialog.dart b/lib/screens/login/login_code_dialog.dart index 76487fee9..107c2af2c 100644 --- a/lib/screens/login/login_code_dialog.dart +++ b/lib/screens/login/login_code_dialog.dart @@ -73,9 +73,7 @@ class _LoginCodeDialogState extends ConsumerState { secret: quickConnectInfo.secret, ); final newSecret = result.body?.secret; - if (result.isSuccessful && - result.body?.authenticated == true && - newSecret != null) { + if (result.isSuccessful && result.body?.authenticated == true && newSecret != null) { widget.onAuthenticated.call(context, newSecret); } else { timer?.reset(); @@ -89,8 +87,7 @@ class _LoginCodeDialogState extends ConsumerState { @override Widget build(BuildContext context) { final code = quickConnectInfo.code; - final serverName = ref.watch(authProvider - .select((value) => value.serverLoginModel?.tempCredentials.serverName)); + final serverName = ref.watch(authProvider.select((value) => value.serverLoginModel?.tempCredentials.serverName)); return Dialog( constraints: const BoxConstraints( maxWidth: 500, @@ -126,10 +123,7 @@ class _LoginCodeDialogState extends ConsumerState { padding: const EdgeInsets.all(12.0), child: Text( code, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith( + style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, wordSpacing: 8, letterSpacing: 8, @@ -143,15 +137,10 @@ class _LoginCodeDialogState extends ConsumerState { ), TextButton.icon( onPressed: () async { - final baseUrl = FladderConfig.baseUrl ?? - ref - .read(authProvider) - .serverLoginModel - ?.tempCredentials - .url; + final baseUrl = + FladderConfig.baseUrl ?? ref.read(authProvider).serverLoginModel?.tempCredentials.url; if (baseUrl != null && baseUrl.isNotEmpty) { - await ext.launchUrl( - context, '$baseUrl/web/#/quickconnect'); + await ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); timer?.reset(); } }, @@ -161,8 +150,7 @@ class _LoginCodeDialogState extends ConsumerState { ], FilledButton( onPressed: () async { - final response = - await ref.read(jellyApiProvider).quickConnectInitiate(); + final response = await ref.read(jellyApiProvider).quickConnectInitiate(); if (response.isSuccessful && response.body != null) { setState(() { quickConnectInfo = response.body!; diff --git a/lib/screens/login/login_screen_credentials.dart b/lib/screens/login/login_screen_credentials.dart index 4ba2198d6..5be45f2d8 100644 --- a/lib/screens/login/login_screen_credentials.dart +++ b/lib/screens/login/login_screen_credentials.dart @@ -30,14 +30,11 @@ class LoginScreenCredentials extends ConsumerStatefulWidget { const LoginScreenCredentials({super.key}); @override - ConsumerState createState() => - _LoginScreenCredentialsState(); + ConsumerState createState() => _LoginScreenCredentialsState(); } -class _LoginScreenCredentialsState - extends ConsumerState { - late final TextEditingController serverTextController = - TextEditingController(text: ''); +class _LoginScreenCredentialsState extends ConsumerState { + late final TextEditingController serverTextController = TextEditingController(text: ''); final usernameController = TextEditingController(); final passwordController = TextEditingController(); final FocusNode focusNode = FocusNode(); @@ -55,28 +52,20 @@ class _LoginScreenCredentialsState @override Widget build(BuildContext context) { - final existingUsers = - ref.watch(authProvider.select((value) => value.accounts)); + final existingUsers = ref.watch(authProvider.select((value) => value.accounts)); final otherCredentials = existingUsers.map((e) => e.credentials).toList(); - final serverCredentials = - ref.watch(authProvider.select((value) => value.serverLoginModel)); + final serverCredentials = ref.watch(authProvider.select((value) => value.serverLoginModel)); final users = serverCredentials?.accounts ?? []; final provider = ref.read(authProvider.notifier); final loading = ref.watch(authProvider.select((value) => value.loading)); - final hasBaseUrl = - ref.watch(authProvider.select((value) => value.hasBaseUrl)); - final urlError = - ref.watch(authProvider.select((value) => value.errorMessage)); - final hasQuickConnect = ref.watch(authProvider - .select((value) => value.serverLoginModel?.hasQuickConnect ?? false)); + final hasBaseUrl = ref.watch(authProvider.select((value) => value.hasBaseUrl)); + final urlError = ref.watch(authProvider.select((value) => value.errorMessage)); + final hasQuickConnect = ref.watch(authProvider.select((value) => value.serverLoginModel?.hasQuickConnect ?? false)); // Note: hidePasswordLogin is a UI preference, not a security control. // It hides the password fields but does not disable password-based authentication on the server. - final hidePasswordLogin = ref - .watch(authProvider.select((value) => value.hidePasswordLogin)) || - ref.watch( - clientSettingsProvider.select((value) => value.hidePasswordLogin)); - final hasSeerrUrl = - ref.watch(authProvider.select((value) => value.hasSeerrUrl)); + final hidePasswordLogin = ref.watch(authProvider.select((value) => value.hidePasswordLogin)) || + ref.watch(clientSettingsProvider.select((value) => value.hidePasswordLogin)); + final hasSeerrUrl = ref.watch(authProvider.select((value) => value.hasSeerrUrl)); ref.listen( authProvider.select((value) => value.serverLoginModel), @@ -93,8 +82,7 @@ class _LoginScreenCredentialsState spacing: 16, children: [ Row( - mainAxisAlignment: - hasBaseUrl ? MainAxisAlignment.center : MainAxisAlignment.start, + mainAxisAlignment: hasBaseUrl ? MainAxisAlignment.center : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, spacing: 8, children: [ @@ -147,8 +135,7 @@ class _LoginScreenCredentialsState AnimatedFadeSize( duration: const Duration(milliseconds: 250), child: loading - ? CircularProgressIndicator( - key: UniqueKey(), strokeCap: StrokeCap.round) + ? CircularProgressIndicator(key: UniqueKey(), strokeCap: StrokeCap.round) : LoginUserGrid( users: users, onPressed: (value) { @@ -190,8 +177,7 @@ class _LoginScreenCredentialsState focusNode: focusNode, autocorrect: false, textInputAction: TextInputAction.send, - onSubmitted: (value) => - enterCredentialsTryLogin?.call(), + onSubmitted: (value) => enterCredentialsTryLogin?.call(), onChanged: (value) => setState(() {}), label: context.localized.password, ), @@ -216,9 +202,7 @@ class _LoginScreenCredentialsState width: 18, height: 18, child: CircularProgressIndicator( - color: Theme.of(context) - .colorScheme - .inversePrimary, + color: Theme.of(context).colorScheme.inversePrimary, strokeCap: StrokeCap.round), ) : Row( @@ -252,9 +236,7 @@ class _LoginScreenCredentialsState if (hasQuickConnect) FilledButton( onPressed: () async { - final result = await ref - .read(jellyApiProvider) - .quickConnectInitiate(); + final result = await ref.read(jellyApiProvider).quickConnectInitiate(); if (result.body != null) { await openLoginCodeDialog( context, @@ -267,9 +249,7 @@ class _LoginScreenCredentialsState }, ); } else { - FladderSnack.show( - context.localized.quickConnectPostFailed, - context: context); + FladderSnack.show(context.localized.quickConnectPostFailed, context: context); } }, child: Row( @@ -299,10 +279,8 @@ class _LoginScreenCredentialsState } Future _openAdvancedLoginOptions() async { - final tempSeerrUrl = - ref.read(authProvider.select((value) => value.tempSeerrUrl)); - final hasSeerrUrl = - ref.read(authProvider.select((value) => value.hasSeerrUrl)); + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final hasSeerrUrl = ref.read(authProvider.select((value) => value.hasSeerrUrl)); final result = await showAdvancedLoginOptionsDialog( context, initialSeerrUrl: tempSeerrUrl, @@ -313,8 +291,7 @@ class _LoginScreenCredentialsState } } - Future Function()? get enterCredentialsTryLogin => - emptyFields() ? null : () => loginUsingCredentials(); + Future Function()? get enterCredentialsTryLogin => emptyFields() ? null : () => loginUsingCredentials(); Future loginUsingCredentials() async { setState(() { @@ -343,8 +320,7 @@ class _LoginScreenCredentialsState return; } - final tempSeerrUrl = - ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); if (tempSeerrUrl != null && tempSeerrUrl.isNotEmpty) { await _tryAuthenticateSeerr(tempSeerrUrl); } @@ -358,8 +334,7 @@ class _LoginScreenCredentialsState try { ref.read(userProvider.notifier).setSeerrServerUrl(seerrUrl); - final tempCookie = ref - .read(authProvider.select((value) => value.tempSeerrSessionCookie)); + final tempCookie = ref.read(authProvider.select((value) => value.tempSeerrSessionCookie)); if (tempCookie != null) { ref.read(userProvider.notifier).setSeerrSessionCookie(tempCookie); ref.read(userProvider.notifier).setSeerrApiKey(''); @@ -401,8 +376,7 @@ class _LoginScreenCredentialsState ref.read(authProvider.notifier).authenticateUsingSecret(secret), ); if (response.isSuccess && context.mounted) { - final tempSeerrUrl = - ref.read(authProvider.select((value) => value.tempSeerrUrl)); + final tempSeerrUrl = ref.read(authProvider.select((value) => value.tempSeerrUrl)); if (tempSeerrUrl != null && tempSeerrUrl.isNotEmpty) { await _tryAuthenticateSeerr(tempSeerrUrl); } @@ -425,21 +399,17 @@ Future loggedInGoToHome(BuildContext context, WidgetRef ref) async { } } -Future _handleLogin( - BuildContext context, AccountModel user, WidgetRef ref) async { +Future _handleLogin(BuildContext context, AccountModel user, WidgetRef ref) async { await ref.read(authProvider.notifier).switchUser(); await ref.read(sharedUtilityProvider).updateAccountInfo(user.copyWith( lastUsed: DateTime.now(), )); - ref - .read(userProvider.notifier) - .updateUser(user.copyWith(lastUsed: DateTime.now())); + ref.read(userProvider.notifier).updateUser(user.copyWith(lastUsed: DateTime.now())); loggedInGoToHome(context, ref); } -void tapLoggedInAccount( - BuildContext context, AccountModel user, WidgetRef ref) async { +void tapLoggedInAccount(BuildContext context, AccountModel user, WidgetRef ref) async { Future loginFunction() => _handleLogin(context, user, ref); switch (user.authMethod) { case Authentication.autoLogin: @@ -457,8 +427,7 @@ void tapLoggedInAccount( if (newPin == user.localPin) { loginFunction(); } else { - FladderSnack.show(context.localized.incorrectPinTryAgain, - context: context); + FladderSnack.show(context.localized.incorrectPinTryAgain, context: context); } }); } diff --git a/lib/screens/settings/widgets/seerr_connection_dialog.dart b/lib/screens/settings/widgets/seerr_connection_dialog.dart index 72e7beccd..59b48dcac 100644 --- a/lib/screens/settings/widgets/seerr_connection_dialog.dart +++ b/lib/screens/settings/widgets/seerr_connection_dialog.dart @@ -48,8 +48,7 @@ class SeerrConnectionDialog extends ConsumerStatefulWidget { const SeerrConnectionDialog({super.key}); @override - ConsumerState createState() => - _SeerrConnectionDialogState(); + ConsumerState createState() => _SeerrConnectionDialogState(); } class _SeerrConnectionDialogState extends ConsumerState { @@ -80,8 +79,7 @@ class _SeerrConnectionDialogState extends ConsumerState { super.initState(); final creds = ref.read(userProvider)?.seerrCredentials; apiKeyController = TextEditingController(text: creds?.apiKey ?? ''); - serverController = TextEditingController( - text: FladderConfig.seerrUrl ?? creds?.serverUrl ?? ''); + serverController = TextEditingController(text: FladderConfig.seerrUrl ?? creds?.serverUrl ?? ''); localEmailController = TextEditingController(); localPasswordController = TextEditingController(); jfUsernameController = TextEditingController(); @@ -187,14 +185,12 @@ class _SeerrConnectionDialogState extends ConsumerState { } String serverUrl; - final hasScheme = - rawUrl.startsWith('http://') || rawUrl.startsWith('https://'); + final hasScheme = rawUrl.startsWith('http://') || rawUrl.startsWith('https://'); if (!hasScheme) { // Probe https first, then http final httpsUrl = normalizeUrl('https://$rawUrl'); final httpUrl = normalizeUrl('http://$rawUrl'); - final result = - await probeSeerrUrl(httpsUrl) ?? await probeSeerrUrl(httpUrl); + final result = await probeSeerrUrl(httpsUrl) ?? await probeSeerrUrl(httpUrl); if (result == null) { if (showError && mounted) { setState(() { @@ -358,8 +354,7 @@ class _SeerrConnectionDialogState extends ConsumerState { } _qcPollAttempts++; try { - final authenticated = - await ref.read(seerrApiProvider).quickConnectCheck(secret); + final authenticated = await ref.read(seerrApiProvider).quickConnectCheck(secret); if (!mounted) return; if (authenticated) { await _quickConnectAuthenticate(secret); @@ -380,8 +375,7 @@ class _SeerrConnectionDialogState extends ConsumerState { }); try { - final cookie = - await ref.read(seerrApiProvider).quickConnectAuthenticate(secret); + final cookie = await ref.read(seerrApiProvider).quickConnectAuthenticate(secret); if (!mounted) return; if (cookie == null || cookie.isEmpty) { setState(() { @@ -466,8 +460,7 @@ class _SeerrConnectionDialogState extends ConsumerState { ), child: Row( children: [ - Icon(IconsaxPlusLinear.warning_2, - color: Theme.of(context).colorScheme.onErrorContainer), + Icon(IconsaxPlusLinear.warning_2, color: Theme.of(context).colorScheme.onErrorContainer), const SizedBox(width: 8), Expanded( child: Text( @@ -484,10 +477,8 @@ class _SeerrConnectionDialogState extends ConsumerState { Widget _loggedInContent() { final serverUrl = ref.read(userProvider)?.seerrCredentials?.serverUrl ?? ''; - final displayName = seerrUser?.displayName ?? - seerrUser?.username ?? - seerrUser?.email ?? - context.localized.seerrUnknownUser; + final displayName = + seerrUser?.displayName ?? seerrUser?.username ?? seerrUser?.email ?? context.localized.seerrUnknownUser; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -505,8 +496,7 @@ class _SeerrConnectionDialogState extends ConsumerState { spacing: 8, children: [ seerrUser?.avatar != null && seerrUser!.avatar!.isNotEmpty - ? CircleAvatar( - backgroundImage: NetworkImage(seerrUser!.avatar!)) + ? CircleAvatar(backgroundImage: NetworkImage(seerrUser!.avatar!)) : CircleAvatar(child: Icon(FladderItemType.person.icon)), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -579,9 +569,7 @@ class _SeerrConnectionDialogState extends ConsumerState { ), ), const SizedBox(width: 8), - IconButton( - onPressed: _addHeader, - icon: const Icon(IconsaxPlusBold.add_circle)), + IconButton(onPressed: _addHeader, icon: const Icon(IconsaxPlusBold.add_circle)), ], ), const SizedBox(height: 8), @@ -647,10 +635,7 @@ class _SeerrConnectionDialogState extends ConsumerState { FilledButton( onPressed: processing ? null : _useApiKey, child: processing - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator()) + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) : Text(context.localized.save), ), ], @@ -689,10 +674,7 @@ class _SeerrConnectionDialogState extends ConsumerState { FilledButton( onPressed: processing ? null : _loginLocal, child: processing - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator()) + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) : Text(context.localized.login), ), ], @@ -730,10 +712,7 @@ class _SeerrConnectionDialogState extends ConsumerState { FilledButton( onPressed: processing ? null : _loginJellyfin, child: processing - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator()) + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) : Text(context.localized.login), ), ], @@ -772,8 +751,7 @@ class _SeerrConnectionDialogState extends ConsumerState { ), TextButton.icon( onPressed: () async { - final baseUrl = FladderConfig.baseUrl ?? - ref.read(userProvider)?.credentials.url; + final baseUrl = FladderConfig.baseUrl ?? ref.read(userProvider)?.credentials.url; if (baseUrl != null && baseUrl.isNotEmpty) { await ext.launchUrl(context, '$baseUrl/web/#/quickconnect'); _qcTimer?.reset(); @@ -790,13 +768,8 @@ class _SeerrConnectionDialogState extends ConsumerState { 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), + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) + : Text(_qcCode != null ? context.localized.refresh : context.localized.quickConnectTitle), ), ), ], @@ -826,9 +799,7 @@ class _SeerrConnectionDialogState extends ConsumerState { child: CircularProgressIndicator(strokeCap: StrokeCap.round), ) else - AnimatedFadeSize( - child: - seerrUser != null ? _loggedInContent() : _authContent()), + AnimatedFadeSize(child: seerrUser != null ? _loggedInContent() : _authContent()), ], ), ), diff --git a/lib/seerr/seerr_chopper_service.chopper.dart b/lib/seerr/seerr_chopper_service.chopper.dart index b774b79b7..0c33846cb 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,12 @@ 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 +139,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client - .send($request); + return client.send($request); } @override @@ -167,8 +161,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client - .send($request); + return client.send($request); } @override @@ -198,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, @@ -216,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, @@ -235,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, @@ -306,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( @@ -573,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, @@ -591,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, @@ -609,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, @@ -649,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, @@ -698,8 +676,7 @@ final class _$SeerrChopperService extends SeerrChopperService { client.baseUrl, parameters: $params, ); - return client - .send($request); + return client.send($request); } @override @@ -725,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, @@ -741,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, @@ -764,8 +735,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send, - SeerrWatchProviderRegion>($request); + return client.send, SeerrWatchProviderRegion>($request); } @override @@ -776,8 +746,7 @@ final class _$SeerrChopperService extends SeerrChopperService { $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override @@ -788,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_models.dart b/lib/seerr/seerr_models.dart index d52377dfe..84122d31c 100644 --- a/lib/seerr/seerr_models.dart +++ b/lib/seerr/seerr_models.dart @@ -127,34 +127,13 @@ enum SeerrSortBy { static const Map _sortValues = { SeerrSortBy.popularityAsc: (movie: 'popularity.asc', tv: 'popularity.asc'), - SeerrSortBy.popularityDesc: ( - movie: 'popularity.desc', - tv: 'popularity.desc' - ), - SeerrSortBy.releaseDateAsc: ( - movie: 'primary_release_date.asc', - tv: 'first_air_date.asc' - ), - SeerrSortBy.releaseDateDesc: ( - movie: 'primary_release_date.desc', - tv: 'first_air_date.desc' - ), - SeerrSortBy.voteAverageAsc: ( - movie: 'vote_average.asc', - tv: 'vote_average.asc' - ), - SeerrSortBy.voteAverageDesc: ( - movie: 'vote_average.desc', - tv: 'vote_average.desc' - ), - SeerrSortBy.titleAsc: ( - movie: 'original_title.asc', - tv: 'original_name.asc' - ), - SeerrSortBy.titleDesc: ( - movie: 'original_title.desc', - tv: 'original_name.desc' - ), + SeerrSortBy.popularityDesc: (movie: 'popularity.desc', tv: 'popularity.desc'), + SeerrSortBy.releaseDateAsc: (movie: 'primary_release_date.asc', tv: 'first_air_date.asc'), + SeerrSortBy.releaseDateDesc: (movie: 'primary_release_date.desc', tv: 'first_air_date.desc'), + SeerrSortBy.voteAverageAsc: (movie: 'vote_average.asc', tv: 'vote_average.asc'), + SeerrSortBy.voteAverageDesc: (movie: 'vote_average.desc', tv: 'vote_average.desc'), + SeerrSortBy.titleAsc: (movie: 'original_title.asc', tv: 'original_name.asc'), + SeerrSortBy.titleDesc: (movie: 'original_title.desc', tv: 'original_name.desc'), }; /// Map to TMDB sort values; some keys differ for movies vs TV. @@ -199,8 +178,7 @@ class SeerrStatus { this.commitsBehind, }); - factory SeerrStatus.fromJson(Map json) => - _$SeerrStatusFromJson(json); + factory SeerrStatus.fromJson(Map json) => _$SeerrStatusFromJson(json); Map toJson() => _$SeerrStatusToJson(this); } @@ -222,8 +200,7 @@ abstract class SeerrUserModel with _$SeerrUserModel { int? tvQuotaDays, }) = _SeerrUserModel; - factory SeerrUserModel.fromJson(Map json) => - _$SeerrUserModelFromJson(json); + factory SeerrUserModel.fromJson(Map json) => _$SeerrUserModelFromJson(json); } @JsonSerializable() @@ -233,8 +210,7 @@ class SeerrUserQuota { SeerrUserQuota({this.movie, this.tv}); - factory SeerrUserQuota.fromJson(Map json) => - _$SeerrUserQuotaFromJson(json); + factory SeerrUserQuota.fromJson(Map json) => _$SeerrUserQuotaFromJson(json); Map toJson() => _$SeerrUserQuotaToJson(this); } @@ -248,11 +224,9 @@ class SeerrQuotaEntry { bool get hasRestrictions => restricted == true || limit != 0; - SeerrQuotaEntry( - {this.days, this.limit, this.used, this.remaining, this.restricted}); + SeerrQuotaEntry({this.days, this.limit, this.used, this.remaining, this.restricted}); - factory SeerrQuotaEntry.fromJson(Map json) => - _$SeerrQuotaEntryFromJson(json); + factory SeerrQuotaEntry.fromJson(Map json) => _$SeerrQuotaEntryFromJson(json); Map toJson() => _$SeerrQuotaEntryToJson(this); } @@ -301,16 +275,13 @@ extension SeerrUserLabelExtension on SeerrUserModel { extension SeerrUserPermissions on SeerrUserModel { int get _permissionValue => permissions ?? 0; - bool get isAdmin => - (_permissionValue & SeerrPermission.admin.bit) == - SeerrPermission.admin.bit; + bool get isAdmin => (_permissionValue & SeerrPermission.admin.bit) == SeerrPermission.admin.bit; bool hasPermission(SeerrPermission permission) => isAdmin ? true : (_permissionValue & permission.bit) == permission.bit; bool get canManageRequests => - hasPermission(SeerrPermission.manageRequests) || - hasPermission(SeerrPermission.requestAdvanced); + hasPermission(SeerrPermission.manageRequests) || hasPermission(SeerrPermission.requestAdvanced); bool get canManageUsers => hasPermission(SeerrPermission.manageUsers); @@ -335,8 +306,7 @@ class SeerrUserSettings { this.originalLanguage, }); - factory SeerrUserSettings.fromJson(Map json) => - _$SeerrUserSettingsFromJson(json); + factory SeerrUserSettings.fromJson(Map json) => _$SeerrUserSettingsFromJson(json); Map toJson() => _$SeerrUserSettingsToJson(this); } @@ -350,8 +320,7 @@ class SeerrUsersResponse { this.pageInfo, }); - factory SeerrUsersResponse.fromJson(Map json) => - _$SeerrUsersResponseFromJson(json); + factory SeerrUsersResponse.fromJson(Map json) => _$SeerrUsersResponseFromJson(json); Map toJson() => _$SeerrUsersResponseToJson(this); } @@ -383,9 +352,7 @@ abstract class SeerrServer { } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) -abstract class SeerrSonarrServer - with _$SeerrSonarrServer - implements SeerrServer { +abstract class SeerrSonarrServer with _$SeerrSonarrServer implements SeerrServer { const factory SeerrSonarrServer({ int? id, String? name, @@ -413,8 +380,7 @@ abstract class SeerrSonarrServer List? activeTags, }) = _SeerrSonarrServer; - factory SeerrSonarrServer.fromJson(Map json) => - _$SeerrSonarrServerFromJson(json); + factory SeerrSonarrServer.fromJson(Map json) => _$SeerrSonarrServerFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -426,14 +392,11 @@ abstract class SeerrSonarrServerResponse with _$SeerrSonarrServerResponse { List? tags, }) = _SeerrSonarrServerResponse; - factory SeerrSonarrServerResponse.fromJson(Map json) => - _$SeerrSonarrServerResponseFromJson(json); + factory SeerrSonarrServerResponse.fromJson(Map json) => _$SeerrSonarrServerResponseFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) -abstract class SeerrRadarrServer - with _$SeerrRadarrServer - implements SeerrServer { +abstract class SeerrRadarrServer with _$SeerrRadarrServer implements SeerrServer { const factory SeerrRadarrServer({ int? id, String? name, @@ -461,8 +424,7 @@ abstract class SeerrRadarrServer List? activeTags, }) = _SeerrRadarrServer; - factory SeerrRadarrServer.fromJson(Map json) => - _$SeerrRadarrServerFromJson(json); + factory SeerrRadarrServer.fromJson(Map json) => _$SeerrRadarrServerFromJson(json); } extension SeerrSonarrServerDefaults on SeerrSonarrServer { @@ -482,8 +444,7 @@ abstract class SeerrRadarrServerResponse with _$SeerrRadarrServerResponse { List? tags, }) = _SeerrRadarrServerResponse; - factory SeerrRadarrServerResponse.fromJson(Map json) => - _$SeerrRadarrServerResponseFromJson(json); + factory SeerrRadarrServerResponse.fromJson(Map json) => _$SeerrRadarrServerResponseFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -493,8 +454,7 @@ abstract class SeerrServiceProfile with _$SeerrServiceProfile { String? name, }) = _SeerrServiceProfile; - factory SeerrServiceProfile.fromJson(Map json) => - _$SeerrServiceProfileFromJson(json); + factory SeerrServiceProfile.fromJson(Map json) => _$SeerrServiceProfileFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -504,8 +464,7 @@ abstract class SeerrServiceTag with _$SeerrServiceTag { String? label, }) = _SeerrServiceTag; - factory SeerrServiceTag.fromJson(Map json) => - _$SeerrServiceTagFromJson(json); + factory SeerrServiceTag.fromJson(Map json) => _$SeerrServiceTagFromJson(json); } @Freezed(copyWith: true, makeCollectionsUnmodifiable: false) @@ -516,8 +475,7 @@ abstract class SeerrRootFolder with _$SeerrRootFolder { String? path, }) = _SeerrRootFolder; - factory SeerrRootFolder.fromJson(Map json) => - _$SeerrRootFolderFromJson(json); + factory SeerrRootFolder.fromJson(Map json) => _$SeerrRootFolderFromJson(json); } @JsonSerializable() @@ -533,8 +491,7 @@ class SeerrContentRating { this.descriptors, }); - factory SeerrContentRating.fromJson(Map json) => - _$SeerrContentRatingFromJson(json); + factory SeerrContentRating.fromJson(Map json) => _$SeerrContentRatingFromJson(json); Map toJson() => _$SeerrContentRatingToJson(this); } @@ -548,8 +505,7 @@ class SeerrCredits { this.crew, }); - factory SeerrCredits.fromJson(Map json) => - _$SeerrCreditsFromJson(json); + factory SeerrCredits.fromJson(Map json) => _$SeerrCreditsFromJson(json); Map toJson() => _$SeerrCreditsToJson(this); } @@ -576,8 +532,7 @@ class SeerrCast { this.internalProfilePath, }); - factory SeerrCast.fromJson(Map json) => - _$SeerrCastFromJson(json); + factory SeerrCast.fromJson(Map json) => _$SeerrCastFromJson(json); Map toJson() => _$SeerrCastToJson(this); } @@ -602,8 +557,7 @@ class SeerrCrew { this.internalProfilePath, }); - factory SeerrCrew.fromJson(Map json) => - _$SeerrCrewFromJson(json); + factory SeerrCrew.fromJson(Map json) => _$SeerrCrewFromJson(json); Map toJson() => _$SeerrCrewToJson(this); } @@ -649,8 +603,7 @@ class SeerrMovieDetails { this.contentRatings, }); - factory SeerrMovieDetails.fromJson(Map json) => - _$SeerrMovieDetailsFromJson(json); + factory SeerrMovieDetails.fromJson(Map json) => _$SeerrMovieDetailsFromJson(json); Map toJson() => _$SeerrMovieDetailsToJson(this); } @@ -704,8 +657,7 @@ class SeerrTvDetails { this.contentRatings, }); - factory SeerrTvDetails.fromJson(Map json) => - _$SeerrTvDetailsFromJson(json); + factory SeerrTvDetails.fromJson(Map json) => _$SeerrTvDetailsFromJson(json); Map toJson() => _$SeerrTvDetailsToJson(this); } @@ -719,14 +671,12 @@ class SeerrGenre { this.name, }); - factory SeerrGenre.fromJson(Map json) => - _$SeerrGenreFromJson(json); + factory SeerrGenre.fromJson(Map json) => _$SeerrGenreFromJson(json); Map toJson() => _$SeerrGenreToJson(this); @override bool operator ==(Object other) => - identical(this, other) || - other is SeerrGenre && runtimeType == other.runtimeType && id == other.id; + identical(this, other) || other is SeerrGenre && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; @@ -742,8 +692,7 @@ class SeerrKeyword { this.name, }); - factory SeerrKeyword.fromJson(Map json) => - _$SeerrKeywordFromJson(json); + factory SeerrKeyword.fromJson(Map json) => _$SeerrKeywordFromJson(json); Map toJson() => _$SeerrKeywordToJson(this); } @@ -769,8 +718,7 @@ class SeerrSeason { this.mediaId, }); - factory SeerrSeason.fromJson(Map json) => - _$SeerrSeasonFromJson(json); + factory SeerrSeason.fromJson(Map json) => _$SeerrSeasonFromJson(json); Map toJson() => _$SeerrSeasonToJson(this); } @@ -793,8 +741,7 @@ class SeerrSeasonDetails { this.episodes, }); - factory SeerrSeasonDetails.fromJson(Map json) => - _$SeerrSeasonDetailsFromJson(json); + factory SeerrSeasonDetails.fromJson(Map json) => _$SeerrSeasonDetailsFromJson(json); Map toJson() => _$SeerrSeasonDetailsToJson(this); } @@ -823,8 +770,7 @@ class SeerrEpisode { this.voteCount, }); - factory SeerrEpisode.fromJson(Map json) => - _$SeerrEpisodeFromJson(json); + factory SeerrEpisode.fromJson(Map json) => _$SeerrEpisodeFromJson(json); Map toJson() => _$SeerrEpisodeToJson(this); } @@ -920,8 +866,7 @@ class SeerrDownloadStatusEpisode { this.id, }); - factory SeerrDownloadStatusEpisode.fromJson(Map json) => - _$SeerrDownloadStatusEpisodeFromJson(json); + factory SeerrDownloadStatusEpisode.fromJson(Map json) => _$SeerrDownloadStatusEpisodeFromJson(json); Map toJson() => _$SeerrDownloadStatusEpisodeToJson(this); } @@ -957,8 +902,7 @@ class SeerrDownloadStatus { return ((size! - (sizeLeft ?? 0)) / size!) * 100; } - factory SeerrDownloadStatus.fromJson(Map json) => - _$SeerrDownloadStatusFromJson(json); + factory SeerrDownloadStatus.fromJson(Map json) => _$SeerrDownloadStatusFromJson(json); Map toJson() => _$SeerrDownloadStatusToJson(this); } @@ -982,11 +926,9 @@ abstract class SeerrMediaInfo with _$SeerrMediaInfo { String? get primaryJellyfinMediaId => jellyfinMediaId4k ?? jellyfinMediaId; - SeerrMediaStatus? get mediaStatus => - status != null ? SeerrMediaStatus.fromRaw(status) : null; + SeerrMediaStatus? get mediaStatus => status != null ? SeerrMediaStatus.fromRaw(status) : null; - factory SeerrMediaInfo.fromJson(Map json) => - _$SeerrMediaInfoFromJson(json); + factory SeerrMediaInfo.fromJson(Map json) => _$SeerrMediaInfoFromJson(json); } @JsonSerializable() @@ -1005,8 +947,7 @@ class SeerrMediaInfoSeason { this.updatedAt, }); - factory SeerrMediaInfoSeason.fromJson(Map json) => - _$SeerrMediaInfoSeasonFromJson(json); + factory SeerrMediaInfoSeason.fromJson(Map json) => _$SeerrMediaInfoSeasonFromJson(json); Map toJson() => _$SeerrMediaInfoSeasonToJson(this); } @@ -1024,8 +965,7 @@ class SeerrExternalIds { this.twitterId, }); - factory SeerrExternalIds.fromJson(Map json) => - _$SeerrExternalIdsFromJson(json); + factory SeerrExternalIds.fromJson(Map json) => _$SeerrExternalIdsFromJson(json); Map toJson() => _$SeerrExternalIdsToJson(this); } @@ -1039,8 +979,7 @@ class SeerrRatingsResponse { this.imdb, }); - factory SeerrRatingsResponse.fromJson(Map json) => - _$SeerrRatingsResponseFromJson(json); + factory SeerrRatingsResponse.fromJson(Map json) => _$SeerrRatingsResponseFromJson(json); Map toJson() => _$SeerrRatingsResponseToJson(this); } @@ -1064,8 +1003,7 @@ class SeerrRtRating { this.url, }); - factory SeerrRtRating.fromJson(Map json) => - _$SeerrRtRatingFromJson(json); + factory SeerrRtRating.fromJson(Map json) => _$SeerrRtRatingFromJson(json); Map toJson() => _$SeerrRtRatingToJson(this); } @@ -1081,8 +1019,7 @@ class SeerrImdbRating { this.criticsScore, }); - factory SeerrImdbRating.fromJson(Map json) => - _$SeerrImdbRatingFromJson(json); + factory SeerrImdbRating.fromJson(Map json) => _$SeerrImdbRatingFromJson(json); Map toJson() => _$SeerrImdbRatingToJson(this); } @@ -1096,8 +1033,7 @@ class SeerrRequestsResponse { this.pageInfo, }); - factory SeerrRequestsResponse.fromJson(Map json) => - _$SeerrRequestsResponseFromJson(json); + factory SeerrRequestsResponse.fromJson(Map json) => _$SeerrRequestsResponseFromJson(json); Map toJson() => _$SeerrRequestsResponseToJson(this); } @@ -1134,8 +1070,7 @@ class SeerrMediaRequest { SeerrRequestStatus get requestStatus => SeerrRequestStatus.fromRaw(status); - factory SeerrMediaRequest.fromJson(Map json) => - _$SeerrMediaRequestFromJson(json); + factory SeerrMediaRequest.fromJson(Map json) => _$SeerrMediaRequestFromJson(json); Map toJson() => _$SeerrMediaRequestToJson(this); } @@ -1147,8 +1082,7 @@ List? _parseRequestSeasons(List? seasons) { .map((season) { if (season is num) return season.toInt(); if (season is Map) { - final value = - season['seasonNumber'] ?? season['season'] ?? season['id']; + final value = season['seasonNumber'] ?? season['season'] ?? season['id']; if (value is num) return value.toInt(); if (value is String) return int.tryParse(value); } @@ -1172,8 +1106,7 @@ class SeerrPageInfo { this.page, }); - factory SeerrPageInfo.fromJson(Map json) => - _$SeerrPageInfoFromJson(json); + factory SeerrPageInfo.fromJson(Map json) => _$SeerrPageInfoFromJson(json); Map toJson() => _$SeerrPageInfoToJson(this); } @@ -1202,8 +1135,7 @@ class SeerrCreateRequestBody { this.userId, }); - factory SeerrCreateRequestBody.fromJson(Map json) => - _$SeerrCreateRequestBodyFromJson(json); + factory SeerrCreateRequestBody.fromJson(Map json) => _$SeerrCreateRequestBodyFromJson(json); Map toJson() => _$SeerrCreateRequestBodyToJson(this); } @@ -1225,8 +1157,7 @@ class SeerrMedia { this.requests, }); - factory SeerrMedia.fromJson(Map json) => - _$SeerrMediaFromJson(json); + factory SeerrMedia.fromJson(Map json) => _$SeerrMediaFromJson(json); Map toJson() => _$SeerrMediaToJson(this); } @@ -1240,8 +1171,7 @@ class SeerrMediaResponse { this.pageInfo, }); - factory SeerrMediaResponse.fromJson(Map json) => - _$SeerrMediaResponseFromJson(json); + factory SeerrMediaResponse.fromJson(Map json) => _$SeerrMediaResponseFromJson(json); Map toJson() => _$SeerrMediaResponseToJson(this); } @@ -1255,8 +1185,7 @@ enum SeerrMediaType { const SeerrMediaType(); - static SeerrMediaType fromString(String mediaType) => - switch (mediaType.toLowerCase()) { + static SeerrMediaType fromString(String mediaType) => switch (mediaType.toLowerCase()) { 'movie' => SeerrMediaType.movie, 'tvshow' || 'tv' => SeerrMediaType.tvshow, 'person' => SeerrMediaType.person, @@ -1299,8 +1228,7 @@ class SeerrDiscoverItem { this.mediaId, }); - factory SeerrDiscoverItem.fromJson(Map json) => - _$SeerrDiscoverItemFromJson(json); + factory SeerrDiscoverItem.fromJson(Map json) => _$SeerrDiscoverItemFromJson(json); Map toJson() => _$SeerrDiscoverItemToJson(this); } @@ -1319,8 +1247,7 @@ class SeerrDiscoverResponse { this.totalResults, }); - factory SeerrDiscoverResponse.fromJson(Map json) => - _$SeerrDiscoverResponseFromJson(json); + factory SeerrDiscoverResponse.fromJson(Map json) => _$SeerrDiscoverResponseFromJson(json); Map toJson() => _$SeerrDiscoverResponseToJson(this); } @@ -1330,8 +1257,7 @@ class SeerrGenreResponse { SeerrGenreResponse({this.genres}); - factory SeerrGenreResponse.fromJson(Map json) => - _$SeerrGenreResponseFromJson(json); + factory SeerrGenreResponse.fromJson(Map json) => _$SeerrGenreResponseFromJson(json); Map toJson() => _$SeerrGenreResponseToJson(this); } @@ -1353,16 +1279,13 @@ class SeerrWatchProvider { this.displayPriority, }); - factory SeerrWatchProvider.fromJson(Map json) => - _$SeerrWatchProviderFromJson(json); + factory SeerrWatchProvider.fromJson(Map json) => _$SeerrWatchProviderFromJson(json); Map toJson() => _$SeerrWatchProviderToJson(this); @override bool operator ==(Object other) => identical(this, other) || - other is SeerrWatchProvider && - runtimeType == other.runtimeType && - providerId == other.providerId; + other is SeerrWatchProvider && runtimeType == other.runtimeType && providerId == other.providerId; @override int get hashCode => providerId.hashCode; @@ -1379,16 +1302,13 @@ class SeerrWatchProviderRegion { SeerrWatchProviderRegion({this.iso31661, this.englishName, this.nativeName}); - factory SeerrWatchProviderRegion.fromJson(Map json) => - _$SeerrWatchProviderRegionFromJson(json); + factory SeerrWatchProviderRegion.fromJson(Map json) => _$SeerrWatchProviderRegionFromJson(json); Map toJson() => _$SeerrWatchProviderRegionToJson(this); @override bool operator ==(Object other) => identical(this, other) || - other is SeerrWatchProviderRegion && - runtimeType == other.runtimeType && - iso31661 == other.iso31661; + other is SeerrWatchProviderRegion && runtimeType == other.runtimeType && iso31661 == other.iso31661; @override int get hashCode => iso31661.hashCode; @@ -1406,16 +1326,13 @@ class SeerrCertification { this.order, }); - factory SeerrCertification.fromJson(Map json) => - _$SeerrCertificationFromJson(json); + factory SeerrCertification.fromJson(Map json) => _$SeerrCertificationFromJson(json); Map toJson() => _$SeerrCertificationToJson(this); @override bool operator ==(Object other) => identical(this, other) || - other is SeerrCertification && - runtimeType == other.runtimeType && - certification == other.certification; + other is SeerrCertification && runtimeType == other.runtimeType && certification == other.certification; @override int get hashCode => certification.hashCode; @@ -1478,8 +1395,7 @@ class SeerrAuthLocalBody { required this.password, }); - factory SeerrAuthLocalBody.fromJson(Map json) => - _$SeerrAuthLocalBodyFromJson(json); + factory SeerrAuthLocalBody.fromJson(Map json) => _$SeerrAuthLocalBodyFromJson(json); Map toJson() => _$SeerrAuthLocalBodyToJson(this); } @@ -1498,8 +1414,7 @@ class SeerrAuthJellyfinBody { this.hostname, }); - factory SeerrAuthJellyfinBody.fromJson(Map json) => - _$SeerrAuthJellyfinBodyFromJson(json); + factory SeerrAuthJellyfinBody.fromJson(Map json) => _$SeerrAuthJellyfinBodyFromJson(json); Map toJson() => _$SeerrAuthJellyfinBodyToJson(this); } @@ -1539,8 +1454,7 @@ class SeerrQuickConnectAuthBody { required this.secret, }); - factory SeerrQuickConnectAuthBody.fromJson(Map json) => - _$SeerrQuickConnectAuthBodyFromJson(json); + factory SeerrQuickConnectAuthBody.fromJson(Map json) => _$SeerrQuickConnectAuthBodyFromJson(json); Map toJson() => _$SeerrQuickConnectAuthBodyToJson(this); } @@ -1576,10 +1490,7 @@ class SeerrCompany { @override bool operator ==(Object other) => - identical(this, other) || - other is SeerrCompany && - runtimeType == other.runtimeType && - id == other.id; + identical(this, other) || other is SeerrCompany && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; @@ -1604,10 +1515,9 @@ class SeerrSearchCompanyResponse { factory SeerrSearchCompanyResponse.fromJson(Map json) { return SeerrSearchCompanyResponse( page: json['page'] as int?, - results: (json['results'] as List?) - ?.map((e) => SeerrCompany.fromJson(e as Map)) - .toList() ?? - [], + results: + (json['results'] as List?)?.map((e) => SeerrCompany.fromJson(e as Map)).toList() ?? + [], totalPages: json['total_pages'] as int?, totalResults: json['total_results'] as int?, ); @@ -1628,24 +1538,27 @@ extension SeerrMovieDetailsExtension on SeerrMovieDetails { } String? get backdropUrl { - if (internalBackdropPath == null || internalBackdropPath!.isEmpty) + if (internalBackdropPath == null || internalBackdropPath!.isEmpty) { return null; + } return '$_tmdbImageBaseUrl$internalBackdropPath'; } } extension SeerrCastExtension on SeerrCast { String? get profileUrl { - if (internalProfilePath == null || internalProfilePath!.isEmpty) + if (internalProfilePath == null || internalProfilePath!.isEmpty) { return null; + } return '$_tmdbProfileBaseUrl$internalProfilePath'; } } extension SeerrCrewExtension on SeerrCrew { String? get profileUrl { - if (internalProfilePath == null || internalProfilePath!.isEmpty) + if (internalProfilePath == null || internalProfilePath!.isEmpty) { return null; + } return '$_tmdbProfileBaseUrl$internalProfilePath'; } } @@ -1657,8 +1570,9 @@ extension SeerrTvDetailsExtension on SeerrTvDetails { } String? get backdropUrl { - if (internalBackdropPath == null || internalBackdropPath!.isEmpty) + if (internalBackdropPath == null || internalBackdropPath!.isEmpty) { return null; + } return '$_tmdbImageBaseUrl$internalBackdropPath'; } } @@ -1684,8 +1598,9 @@ extension SeerrDiscoverItemExtension on SeerrDiscoverItem { } String? get backdropUrl { - if (internalBackdropPath == null || internalBackdropPath!.isEmpty) + if (internalBackdropPath == null || internalBackdropPath!.isEmpty) { return null; + } return '$_tmdbImageBaseUrl$internalBackdropPath'; } }