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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/libs/nextcloud/src/main/res/values-night/styles.xml b/libs/nextcloud/src/main/res/values-night/styles.xml
deleted file mode 100644
index 71ecd1bb..00000000
--- a/libs/nextcloud/src/main/res/values-night/styles.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libs/nextcloud/src/main/res/values/styles.xml b/libs/nextcloud/src/main/res/values/styles.xml
deleted file mode 100644
index 3b528b74..00000000
--- a/libs/nextcloud/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
\ No newline at end of file