From d48ac28b8f76a6ec2012100ed1828640a3993652 Mon Sep 17 00:00:00 2001 From: Harry Andreolas Date: Sat, 31 Jan 2026 14:37:30 +0200 Subject: [PATCH] feat: add email support text on seerr login --- .../core/ui/manager/IntentManager.android.kt | 36 +++++++- .../kotlin/com/divinelink/core/ui/TestTags.kt | 1 + .../core/ui/manager/IntentManager.kt | 6 ++ .../core/ui/manager/IntentManager.native.kt | 40 +++++++++ .../JellyseerrSettingsScreenTest.kt | 32 ++++++++ .../composeResources/values/strings.xml | 7 ++ .../jellyseerr/JellyseerrLoginContent.kt | 10 ++- .../jellyseerr/ui/EmailSupportField.kt | 82 +++++++++++++++++++ 8 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/ui/EmailSupportField.kt diff --git a/core/ui/src/androidMain/kotlin/com/divinelink/core/ui/manager/IntentManager.android.kt b/core/ui/src/androidMain/kotlin/com/divinelink/core/ui/manager/IntentManager.android.kt index 5c7cf817d..76237a5bb 100644 --- a/core/ui/src/androidMain/kotlin/com/divinelink/core/ui/manager/IntentManager.android.kt +++ b/core/ui/src/androidMain/kotlin/com/divinelink/core/ui/manager/IntentManager.android.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.provider.Settings +import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri @@ -60,7 +61,40 @@ internal class AndroidIntentManager( data = Uri.fromParts(PACKAGE_SCHEME, context.packageName, null) } - context.startActivity(intent, null) + try { + context.startActivity(intent, null) + } catch (_: Exception) { + Toast.makeText(context, "Couldn't not open app settings", Toast.LENGTH_SHORT).show() + } + } + + override fun launchEmail( + email: String, + subject: String?, + body: String?, + ) { + val emailIntent = Intent(Intent.ACTION_SENDTO).apply { + val uriString = buildString { + append("mailto:$email") + + val params = mutableListOf() + subject?.let { params.add("subject=${Uri.encode(it)}") } + body?.let { params.add("body=${Uri.encode(it)}") } + + if (params.isNotEmpty()) { + append("?") + append(params.joinToString("&")) + } + } + + data = uriString.toUri() + } + + try { + context.startActivity(emailIntent) + } catch (_: Exception) { + Toast.makeText(context, "No email app installed", Toast.LENGTH_SHORT).show() + } } companion object { diff --git a/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/TestTags.kt b/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/TestTags.kt index f0fbd820b..4f988a83f 100644 --- a/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/TestTags.kt +++ b/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/TestTags.kt @@ -205,6 +205,7 @@ object TestTags { const val USERNAME_TEXT_FIELD = "Username input" const val PASSWORD_TEXT_FIELD = "Password input" + const val EMAIL_SUPPORT = "Email support field" const val JELLYSEERR_LOGIN_BUTTON = "Jellyseerr Login Button" const val JELLYSEERR_LOGOUT_BUTTON = "Jellyseerr Logout Button" } diff --git a/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/manager/IntentManager.kt b/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/manager/IntentManager.kt index eddf3983c..faec50ce5 100644 --- a/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/manager/IntentManager.kt +++ b/core/ui/src/commonMain/kotlin/com/divinelink/core/ui/manager/IntentManager.kt @@ -14,6 +14,12 @@ interface IntentManager { fun shareErrorReport(throwable: Throwable) + fun launchEmail( + email: String, + subject: String?, + body: String?, + ) + fun navigateToAppSettings() } diff --git a/core/ui/src/nativeMain/kotlin/com/divinelink/core/ui/manager/IntentManager.native.kt b/core/ui/src/nativeMain/kotlin/com/divinelink/core/ui/manager/IntentManager.native.kt index 95cdc51b1..585f3cd52 100644 --- a/core/ui/src/nativeMain/kotlin/com/divinelink/core/ui/manager/IntentManager.native.kt +++ b/core/ui/src/nativeMain/kotlin/com/divinelink/core/ui/manager/IntentManager.native.kt @@ -2,6 +2,13 @@ package com.divinelink.core.ui.manager import androidx.compose.runtime.Composable import com.divinelink.core.commons.provider.BuildConfigProvider +import kotlinx.cinterop.BetaInteropApi +import platform.Foundation.NSCharacterSet +import platform.Foundation.NSString +import platform.Foundation.NSURL +import platform.Foundation.URLQueryAllowedCharacterSet +import platform.Foundation.create +import platform.Foundation.stringByAddingPercentEncodingWithAllowedCharacters import platform.UIKit.UIActivityViewController import platform.UIKit.UIApplication import platform.UIKit.popoverPresentationController @@ -49,6 +56,39 @@ class IOSIntentManager( .toString(), ) + override fun launchEmail( + email: String, + subject: String?, + body: String?, + ) { + val urlString = buildString { + append("mailto:$email") + + val params = mutableListOf() + subject?.let { params.add("subject=${it.encodeURLComponent()}") } + body?.let { params.add("body=${it.encodeURLComponent()}") } + + if (params.isNotEmpty()) { + append("?") + append(params.joinToString("&")) + } + } + + val url = NSURL.URLWithString(urlString) ?: return + + UIApplication.sharedApplication.openURL(url, options = emptyMap()) { success -> + if (!success) { + println("Failed to open email app") + } + } + } + + @OptIn(BetaInteropApi::class) + private fun String.encodeURLComponent(): String = + NSString.create(string = this).stringByAddingPercentEncodingWithAllowedCharacters( + NSCharacterSet.URLQueryAllowedCharacterSet, + ) ?: this + override fun navigateToAppSettings() { // Do nothing } diff --git a/feature/settings/src/androidHostTest/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrSettingsScreenTest.kt b/feature/settings/src/androidHostTest/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrSettingsScreenTest.kt index 5e0d252d8..50be776bd 100644 --- a/feature/settings/src/androidHostTest/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrSettingsScreenTest.kt +++ b/feature/settings/src/androidHostTest/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrSettingsScreenTest.kt @@ -139,8 +139,16 @@ class JellyseerrSettingsScreenTest : ComposeTest() { TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasText("Jellyfin"), + ) + onNodeWithText("Jellyfin").assertIsDisplayed().performClick() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasTestTag(TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON), + ) + onNodeWithTag( TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() @@ -225,8 +233,16 @@ class JellyseerrSettingsScreenTest : ComposeTest() { TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasText("Jellyseerr"), + ) + onNodeWithText("Jellyseerr").assertIsDisplayed().performClick() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasTestTag(TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON), + ) + onNodeWithTag( TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() @@ -313,8 +329,16 @@ class JellyseerrSettingsScreenTest : ComposeTest() { TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasText("Emby"), + ) + onNodeWithText("Emby").assertIsDisplayed().performClick() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasTestTag(TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON), + ) + onNodeWithTag( TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() @@ -465,8 +489,16 @@ class JellyseerrSettingsScreenTest : ComposeTest() { TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasText("Emby"), + ) + onNodeWithText("Emby").assertIsDisplayed().performClick() + onNodeWithTag(TestTags.Settings.Jellyseerr.INITIAL_CONTENT).performScrollToNode( + hasTestTag(TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON), + ) + onNodeWithTag( TestTags.Settings.Jellyseerr.JELLYSEERR_LOGIN_BUTTON, ).assertIsNotEnabled() diff --git a/feature/settings/src/commonMain/composeResources/values/strings.xml b/feature/settings/src/commonMain/composeResources/values/strings.xml index d38c1ec7b..95023348b 100644 --- a/feature/settings/src/commonMain/composeResources/values/strings.xml +++ b/feature/settings/src/commonMain/composeResources/values/strings.xml @@ -87,6 +87,13 @@ Episodes Seasons + support@scenepeek.app + Trouble connecting to Seerr + "Please specify your login method, server versions and attach the error logs to help identify the issue."\n\nSeerr version: (enter your Seerr server version)\nJellyfin version: (enter your Jellyfin version if applicable)\n%1$s + + This is an experimental feature.\nIf you have trouble connecting, please get in contact at + to troubleshoot the issue. + None Account diff --git a/feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrLoginContent.kt b/feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrLoginContent.kt index f7ca855b7..eba4d63eb 100644 --- a/feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrLoginContent.kt +++ b/feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/JellyseerrLoginContent.kt @@ -40,12 +40,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentType import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.sp import com.divinelink.core.designsystem.component.ScenePeekLazyColumn import com.divinelink.core.designsystem.theme.AppTheme import com.divinelink.core.designsystem.theme.dimensions @@ -55,6 +53,7 @@ import com.divinelink.core.ui.PasswordOutlinedTextField import com.divinelink.core.ui.Previews import com.divinelink.core.ui.TestTags import com.divinelink.feature.settings.app.account.jellyseerr.preview.JellyseerrLoginStatePreviewParameterProvider +import com.divinelink.feature.settings.app.account.jellyseerr.ui.EmailSupportField import com.divinelink.feature.settings.resources.Res import com.divinelink.feature.settings.resources.feature_settings_email import com.divinelink.feature.settings.resources.feature_settings_jellyseerr_address_placeholder @@ -131,6 +130,10 @@ fun JellyseerrLoginContent( ) } + item { + EmailSupportField() + } + item { Spacer(modifier = Modifier.height(MaterialTheme.dimensions.keyline_96)) } @@ -210,8 +213,7 @@ private fun AuthMethodButton( Text( text = method.displayName, color = textColor, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, + style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center, ) } diff --git a/feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/ui/EmailSupportField.kt b/feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/ui/EmailSupportField.kt new file mode 100644 index 000000000..7244d27cc --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/divinelink/feature/settings/app/account/jellyseerr/ui/EmailSupportField.kt @@ -0,0 +1,82 @@ +package com.divinelink.feature.settings.app.account.jellyseerr.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import com.divinelink.core.designsystem.theme.dimensions +import com.divinelink.core.ui.TestTags +import com.divinelink.core.ui.composition.LocalIntentManager +import com.divinelink.core.ui.rememberConfigProvider +import com.divinelink.feature.settings.resources.Res +import com.divinelink.feature.settings.resources.seerr_login_info_placeholder_pt1 +import com.divinelink.feature.settings.resources.seerr_login_info_placeholder_pt2 +import com.divinelink.feature.settings.resources.seerr_support_email_body +import com.divinelink.feature.settings.resources.seerr_support_email_subject +import com.divinelink.feature.settings.resources.support_email +import org.jetbrains.compose.resources.stringResource + +@Composable +fun EmailSupportField() { + val intentManager = LocalIntentManager.current + val configProvider = rememberConfigProvider() + + val email = stringResource(Res.string.support_email) + val subject = stringResource(Res.string.seerr_support_email_subject) + val body = stringResource( + Res.string.seerr_support_email_body, + configProvider.versionData, + ) + + Row( + modifier = Modifier + .testTag(TestTags.Settings.Jellyseerr.EMAIL_SUPPORT) + .clip(MaterialTheme.shapes.medium) + .clickable { + intentManager.launchEmail( + email = email, + subject = subject, + body = body, + ) + }, + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.dimensions.keyline_4), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + ) + Text( + style = MaterialTheme.typography.labelMedium, + fontStyle = FontStyle.Italic, + textAlign = TextAlign.Center, + text = buildAnnotatedString { + append(stringResource(Res.string.seerr_login_info_placeholder_pt1)) + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + ), + ) { + append(email) + } + append(stringResource(Res.string.seerr_login_info_placeholder_pt2)) + }, + ) + } +}