From 4485b59ade2620bfd3868e9241c9b32e0e58c828 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat, 19 Apr 2025 17:57:49 +0200 Subject: [PATCH] Port Nextcloud login UI to compose --- app/ui/src/main/AndroidManifest.xml | 1 - gradle/libs.versions.toml | 2 +- libs/nextcloud/build.gradle.kts | 7 +- libs/nextcloud/src/main/AndroidManifest.xml | 5 +- .../mm20/launcher2/nextcloud/LoginActivity.kt | 213 ++++++++++++++++-- .../res/layout/activity_nextcloud_login.xml | 44 ---- .../src/main/res/values-night/styles.xml | 9 - libs/nextcloud/src/main/res/values/styles.xml | 11 - 8 files changed, 199 insertions(+), 93 deletions(-) delete mode 100644 libs/nextcloud/src/main/res/layout/activity_nextcloud_login.xml delete mode 100644 libs/nextcloud/src/main/res/values-night/styles.xml delete mode 100644 libs/nextcloud/src/main/res/values/styles.xml diff --git a/app/ui/src/main/AndroidManifest.xml b/app/ui/src/main/AndroidManifest.xml index 88cb4a10..a3fd77e1 100644 --- a/app/ui/src/main/AndroidManifest.xml +++ b/app/ui/src/main/AndroidManifest.xml @@ -54,7 +54,6 @@ android:label="@string/settings" android:launchMode="singleTop" android:parentActivityName=".launcher.LauncherActivity" - android:taskAffinity="de.mm20.launcher2.settings" android:theme="@style/SettingsTheme.NoActionBar" android:enableOnBackInvokedCallback="true" > diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c586008..6f576206 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ pluginSdk = "2.2.0-SNAPSHOT" gradle = "8.1.2" android-gradle-plugin = "8.6.1" -ksp-gradle-plugin = "2.1.10-1.0.29" +ksp-gradle-plugin = "2.1.20-2.0.0" kotlin = "2.1.20" kotlinx-coroutines = "1.9.0" diff --git a/libs/nextcloud/build.gradle.kts b/libs/nextcloud/build.gradle.kts index 81a75a3b..025b9493 100644 --- a/libs/nextcloud/build.gradle.kts +++ b/libs/nextcloud/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.plugin.compose) } android { @@ -28,7 +29,7 @@ android { } buildFeatures { - viewBinding = true + compose = true } kotlinOptions { @@ -39,11 +40,11 @@ android { dependencies { implementation(libs.bundles.kotlin) + implementation(libs.androidx.activitycompose) + implementation(libs.androidx.compose.material3) implementation(libs.androidx.core) implementation(libs.androidx.appcompat) - implementation(libs.materialcomponents.core) implementation(libs.androidx.browser) - implementation(libs.androidx.constraintlayout.views) implementation(libs.androidx.securitycrypto) implementation(libs.bundles.androidx.lifecycle) diff --git a/libs/nextcloud/src/main/AndroidManifest.xml b/libs/nextcloud/src/main/AndroidManifest.xml index 38ad44a8..11e5b2d8 100644 --- a/libs/nextcloud/src/main/AndroidManifest.xml +++ b/libs/nextcloud/src/main/AndroidManifest.xml @@ -2,10 +2,11 @@ + android:launchMode="singleTop" + android:enableOnBackInvokedCallback="true"/> \ No newline at end of file diff --git a/libs/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/LoginActivity.kt b/libs/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/LoginActivity.kt index ebf3e9d1..5830cbd8 100644 --- a/libs/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/LoginActivity.kt +++ b/libs/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/LoginActivity.kt @@ -2,41 +2,135 @@ package de.mm20.launcher2.nextcloud import android.app.Activity import android.os.Bundle -import android.view.LayoutInflater import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope -import de.mm20.launcher2.nextcloud.databinding.ActivityNextcloudLoginBinding -import kotlinx.coroutines.* +import kotlinx.coroutines.launch class LoginActivity : AppCompatActivity() { private val nextcloudClient = NextcloudApiHelper(this) - private lateinit var binding: ActivityNextcloudLoginBinding - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityNextcloudLoginBinding.inflate(LayoutInflater.from(this)) - setContentView(binding.root) - binding.nextButton.setOnClickListener { - binding.serverUrlInputLayout.error = null - lifecycleScope.launch { - var url = binding.serverUrlInput.text.toString() - if (!(url.startsWith("http://") || url.startsWith("https://"))) { - url = "https://$url" + + setContent { + MaterialTheme( + colorScheme = if (isSystemInDarkTheme()) nextcloudDark else nextcloudLight + ) { + var nextcloudUrl by remember { mutableStateOf("") } + var error by remember { mutableStateOf(null) } + var loading by remember { mutableStateOf(false) } + + val dark = isSystemInDarkTheme() + + LaunchedEffect(dark) { + enableEdgeToEdge( + statusBarStyle = if (dark) SystemBarStyle.dark(0) else SystemBarStyle.light( + 0, + 0x33000000.toInt() + ), + navigationBarStyle = if (dark) SystemBarStyle.dark(0) else SystemBarStyle.light( + 0, + 0x33000000.toInt() + ), + ) } - if (url.isBlank()) { - binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_url_empty) - return@launch - } - if (nextcloudClient.checkNextcloudInstallation(url)) { - openLoginPage(url) - } else { - binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_invalid_url) + + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .padding(32.dp) + .systemBarsPadding() + .imePadding(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painterResource(R.drawable.ic_nextcloud_logo), + contentDescription = "Nextcloud Logo", + colorFilter = ColorFilter.tint( + MaterialTheme.colorScheme.primary, + ) + ) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + label = { + Text(stringResource(R.string.nextcloud_server_url)) + }, + value = nextcloudUrl, + onValueChange = { nextcloudUrl = it }, + enabled = !loading, + isError = error != null, + supportingText = error?.let { { Text(it) } }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + ) + ) + Button( + modifier = Modifier.fillMaxWidth(), + enabled = nextcloudUrl.isNotBlank() && !loading, + onClick = { + lifecycleScope.launch { + loading = true + error = null + var url = nextcloudUrl + if (!(url.startsWith("http://") || url.startsWith("https://"))) { + url = "https://$url" + } + if (nextcloudClient.checkNextcloudInstallation(url)) { + openLoginPage(url) + } else { + error = getString(R.string.nextcloud_server_invalid_url) + } + loading = false + } + } + ) { + Text(stringResource(R.string.login_flow_continue)) + } } + } } } @@ -45,7 +139,10 @@ class LoginActivity : AppCompatActivity() { val webView = WebView(this) webView.settings.userAgentString = getString(R.string.app_name) webView.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { if (request?.url?.scheme == "nc") { val path = request.url?.path?.trim('/') ?: run { setResult(0) @@ -82,4 +179,76 @@ class LoginActivity : AppCompatActivity() { webView.loadUrl("$url/index.php/login/flow", headers) } + + private val nextcloudLight: ColorScheme + get() = lightColorScheme( + primary = Color(0xFF00639B), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFCEE5FF), + onPrimaryContainer = Color(0xFF001D33), + inversePrimary = Color(0xFF96CBFF), + secondary = Color(0xFF51606F), + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFFD4E4F6), + onSecondaryContainer = Color(0xFF0D1D2A), + tertiary = Color(0xFF68587A), + onTertiary = Color(0xFFFFFFFF), + tertiaryContainer = Color(0xFFEEDBFF), + onTertiaryContainer = Color(0xFF231533), + surface = Color(0xFFF9F9FC), + surfaceBright = Color(0xFFF9F9FC), + surfaceDim = Color(0xFFD9DADD), + surfaceContainer = Color(0xFFEDEEF1), + surfaceContainerHighest = Color(0xFFE2E2E5), + surfaceContainerHigh = Color(0xFFE8E8EB), + surfaceContainerLow = Color(0xFFF3F3F6), + surfaceContainerLowest = Color(0xFFFFFFFF), + onSurface = Color(0xFF1A1C1E), + onSurfaceVariant = Color(0xFF42474E), + inverseSurface = Color(0xFF2F3133), + inverseOnSurface = Color(0xFFF0F0F3), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD5), + onErrorContainer = Color(0xFF410002), + outline = Color(0xFF72787F), + outlineVariant = Color(0xFFC2C7CF), + scrim = Color(0xFF000000), + ) + + private val nextcloudDark: ColorScheme + get() = darkColorScheme( + primary = Color(0xFF96CBFF), + onPrimary = Color(0xFF003353), + primaryContainer = Color(0xFF004A76), + onPrimaryContainer = Color(0xFFCEE5FF), + inversePrimary = Color(0xFF00639B), + secondary = Color(0xFFB9C8DA), + onSecondary = Color(0xFF23323F), + secondaryContainer = Color(0xFF394857), + onSecondaryContainer = Color(0xFFD4E4F6), + tertiary = Color(0xFFD3BFE6), + onTertiary = Color(0xFF382A49), + tertiaryContainer = Color(0xFF4F4061), + onTertiaryContainer = Color(0xFFEEDBFF), + surface = Color(0xFF1A1C1E), + surfaceBright = Color(0xFF37393C), + surfaceDim = Color(0xFF111416), + surfaceContainer = Color(0xFF1E2022), + surfaceContainerHighest = Color(0xFF333537), + surfaceContainerHigh = Color(0xFF282A2D), + surfaceContainerLow = Color(0xFF1A1C1E), + surfaceContainerLowest = Color(0xFF0C0E11), + onSurface = Color(0xFFE2E2E5), + onSurfaceVariant = Color(0xFFC2C7CF), + inverseSurface = Color(0xFFE2E2E5), + inverseOnSurface = Color(0xFF2F3133), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690004), + errorContainer = Color(0xFF930009), + onErrorContainer = Color(0xFFFFB4AB), + outline = Color(0xFF8C9198), + outlineVariant = Color(0xFF42474E), + scrim = Color(0xFF000000), + ) } \ No newline at end of file diff --git a/libs/nextcloud/src/main/res/layout/activity_nextcloud_login.xml b/libs/nextcloud/src/main/res/layout/activity_nextcloud_login.xml deleted file mode 100644 index 017f3a45..00000000 --- a/libs/nextcloud/src/main/res/layout/activity_nextcloud_login.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - -