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:label="@string/settings"
android:launchMode="singleTop" android:launchMode="singleTop"
android:parentActivityName=".launcher.LauncherActivity" android:parentActivityName=".launcher.LauncherActivity"
android:taskAffinity="de.mm20.launcher2.settings"
android:theme="@style/SettingsTheme.NoActionBar" android:theme="@style/SettingsTheme.NoActionBar"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
> >

View File

@ -10,7 +10,7 @@ pluginSdk = "2.2.0-SNAPSHOT"
gradle = "8.1.2" gradle = "8.1.2"
android-gradle-plugin = "8.6.1" 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" kotlin = "2.1.20"
kotlinx-coroutines = "1.9.0" kotlinx-coroutines = "1.9.0"

View File

@ -1,6 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.plugin.compose)
} }
android { android {
@ -28,7 +29,7 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding = true compose = true
} }
kotlinOptions { kotlinOptions {
@ -39,11 +40,11 @@ android {
dependencies { dependencies {
implementation(libs.bundles.kotlin) implementation(libs.bundles.kotlin)
implementation(libs.androidx.activitycompose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.materialcomponents.core)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)
implementation(libs.androidx.constraintlayout.views)
implementation(libs.androidx.securitycrypto) implementation(libs.androidx.securitycrypto)
implementation(libs.bundles.androidx.lifecycle) implementation(libs.bundles.androidx.lifecycle)

View File

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

View File

@ -2,41 +2,135 @@ package de.mm20.launcher2.nextcloud
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity 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 androidx.lifecycle.lifecycleScope
import de.mm20.launcher2.nextcloud.databinding.ActivityNextcloudLoginBinding import kotlinx.coroutines.launch
import kotlinx.coroutines.*
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
private val nextcloudClient = NextcloudApiHelper(this) private val nextcloudClient = NextcloudApiHelper(this)
private lateinit var binding: ActivityNextcloudLoginBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityNextcloudLoginBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root) setContent {
binding.nextButton.setOnClickListener { MaterialTheme(
binding.serverUrlInputLayout.error = null 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()
),
)
}
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 { lifecycleScope.launch {
var url = binding.serverUrlInput.text.toString() loading = true
error = null
var url = nextcloudUrl
if (!(url.startsWith("http://") || url.startsWith("https://"))) { if (!(url.startsWith("http://") || url.startsWith("https://"))) {
url = "https://$url" url = "https://$url"
} }
if (url.isBlank()) {
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_url_empty)
return@launch
}
if (nextcloudClient.checkNextcloudInstallation(url)) { if (nextcloudClient.checkNextcloudInstallation(url)) {
openLoginPage(url) openLoginPage(url)
} else { } else {
binding.serverUrlInputLayout.error = getString(R.string.nextcloud_server_invalid_url) 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) val webView = WebView(this)
webView.settings.userAgentString = getString(R.string.app_name) webView.settings.userAgentString = getString(R.string.app_name)
webView.webViewClient = object : WebViewClient() { 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") { if (request?.url?.scheme == "nc") {
val path = request.url?.path?.trim('/') ?: run { val path = request.url?.path?.trim('/') ?: run {
setResult(0) setResult(0)
@ -82,4 +179,76 @@ class LoginActivity : AppCompatActivity() {
webView.loadUrl("$url/index.php/login/flow", headers) 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>