Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,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",
Expand Down Expand Up @@ -2202,6 +2204,8 @@
"@seerrAuthLocal": {},
"seerrAuthJellyfin": "Jellyfin",
"@seerrAuthJellyfin": {},
"seerrAuthQuickConnect": "QuickConnect",
"@seerrAuthQuickConnect": {},
"seerrUserFetchFailed": "Failed to fetch user from Seerr",
"@seerrUserFetchFailed": {},
"seerrEnterServerUrlFirst": "Enter a Seerr server URL first",
Expand Down
19 changes: 19 additions & 0 deletions lib/providers/seerr_service_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,25 @@ class SeerrService {
return _requireSessionCookie(response, label: 'Jellyfin');
}

Future<SeerrQuickConnectInitResponse?> quickConnectInitiate() async {
final response = await _api.quickConnectInitiate();
if (!response.isSuccessful) return null;
return response.body;
}

Future<bool> quickConnectCheck(String secret) async {
final response = await _api.quickConnectCheck(secret);
return response.body?.authenticated ?? false;
}

Future<String?> quickConnectAuthenticate(String secret) async {
final response = await _api.quickConnectAuthenticate(
SeerrQuickConnectAuthBody(secret: secret),
);
if (!response.isSuccessful) return null;
return _extractSessionCookie(response);
}

Future<void> logout() async => await _api.logout();

Future<Response<dynamic>> _authenticateJellyfin(
Expand Down
26 changes: 19 additions & 7 deletions lib/screens/login/login_code_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ class LoginCodeDialog extends ConsumerStatefulWidget {
class _LoginCodeDialogState extends ConsumerState<LoginCodeDialog> {
late QuickConnectResult quickConnectInfo = widget.quickConnectInfo;

static const _maxPollAttempts = 300; // ~5 minutes at 1s interval
RestartableTimer? timer;
int _pollAttempts = 0;

@override
void initState() {
Expand All @@ -56,14 +58,24 @@ class _LoginCodeDialogState extends ConsumerState<LoginCodeDialog> {

void createTimer() {
timer?.cancel();
_pollAttempts = 0;
timer = RestartableTimer(const Duration(seconds: 1), () async {
final result = await ref.read(jellyApiProvider).quickConnectConnectGet(
secret: quickConnectInfo.secret,
);
final newSecret = result.body?.secret;
if (result.isSuccessful && result.body?.authenticated == true && newSecret != null) {
widget.onAuthenticated.call(context, newSecret);
} else {
if (_pollAttempts >= _maxPollAttempts) {
timer?.cancel();
return;
}
_pollAttempts++;
try {
final result = await ref.read(jellyApiProvider).quickConnectConnectGet(
secret: quickConnectInfo.secret,
);
final newSecret = result.body?.secret;
if (result.isSuccessful && result.body?.authenticated == true && newSecret != null) {
widget.onAuthenticated.call(context, newSecret);
} else {
timer?.reset();
}
} catch (_) {
timer?.reset();
}
});
Expand Down
42 changes: 27 additions & 15 deletions lib/screens/login/login_screen_credentials.dart
Original file line number Diff line number Diff line change
Expand Up @@ -377,25 +377,31 @@ class _LoginScreenCredentialsState extends ConsumerState<LoginScreenCredentials>

Future<void> _tryAuthenticateSeerr(String seerrUrl) async {
try {
final username = usernameController.text.trim();
final password = passwordController.text;

final effectiveSeerrUrl = FladderConfig.seerrBaseUrl ?? seerrUrl;
ref.read(userProvider.notifier).setSeerrServerUrl(effectiveSeerrUrl);

final tempCookie = ref.read(authProvider.select((value) => value.tempSeerrSessionCookie));
final cookie = tempCookie ??
await ref.read(seerrApiProvider).authenticateJellyfin(
username: username,
password: password,
);

ref.read(userProvider.notifier).setSeerrSessionCookie(cookie);
ref.read(userProvider.notifier).setSeerrApiKey('');
ref.read(authProvider.notifier).setTempSeerrSessionCookie(null);
if (tempCookie != null) {
ref.read(userProvider.notifier).setSeerrSessionCookie(tempCookie);
ref.read(userProvider.notifier).setSeerrApiKey('');
ref.read(authProvider.notifier).setTempSeerrSessionCookie(null);
if (context.mounted) {
FladderSnack.show(context.localized.seerrLoggedIn, context: context);
}
return;
}

if (context.mounted) {
FladderSnack.show(context.localized.seerrLoggedIn, context: context);
final password = passwordController.text;
if (password.isNotEmpty) {
final cookie = await ref.read(seerrApiProvider).authenticateJellyfin(
username: usernameController.text.trim(),
password: password,
);
ref.read(userProvider.notifier).setSeerrSessionCookie(cookie);
ref.read(userProvider.notifier).setSeerrApiKey('');
if (context.mounted) {
FladderSnack.show(context.localized.seerrLoggedIn, context: context);
}
}
} catch (e) {
if (context.mounted) {
Expand All @@ -415,7 +421,13 @@ class _LoginScreenCredentialsState extends ConsumerState<LoginScreenCredentials>
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;
Expand Down
Loading
Loading