Port Nextcloud login UI to compose

This commit is contained in:
MM20 2025-04-19 17:57:49 +02:00
parent 9822777acb
commit 4485b59ade
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 199 additions and 93 deletions

View File

@ -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"
>

View File

@ -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"

View File

@ -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)

View File

@ -2,10 +2,11 @@
<application>
<activity
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
android:name="de.mm20.launcher2.nextcloud.LoginActivity"
android:label="@string/preference_nextcloud"
android:taskAffinity="de.mm20.launcher2.nextcloud"
android:theme="@style/NextcloudLoginTheme" />
android:launchMode="singleTop"
android:enableOnBackInvokedCallback="true"/>
</application>
</manifest>

View File

@ -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<String?>(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),
)
}

View File

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="32dp"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_nextcloud_logo" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/serverUrlInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:hint="@string/nextcloud_server_url"
app:helperTextEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/serverUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/nextButton"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_flow_continue" />
</LinearLayout>
</FrameLayout>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NextcloudLoginTheme" parent="Theme.MaterialComponents.NoActionBar">
<item name="colorAccent">#0082c9</item>
<item name="colorPrimary">#0082c9</item>
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
</style>
</resources>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="NextcloudLoginTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="colorAccent">#0082c9</item>
<item name="colorPrimary">#0082c9</item>
<item name="colorPrimaryDark">@color/settings_color_primary_dark</item>
<item name="android:navigationBarColor">@color/settings_color_primary_dark</item>
</style>
</resources>