From ca4188c2d3924dbb6181b3912a210090695104de Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 4 Mar 2024 12:32:34 -0800 Subject: [PATCH 01/27] fix(build): add compose and dependencies --- app/build.gradle | 25 ++++++++++++++++++++++--- build.gradle | 2 +- gradle.properties | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2535cc9a..a180a0ac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,18 +63,25 @@ android { buildFeatures { viewBinding true + buildConfig true + compose true } + lint { abortOnError false } + composeOptions { + kotlinCompilerExtensionVersion "1.5.10" + } + namespace 'net.opendasharchive.openarchive' } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.21" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0" implementation "androidx.core:core-ktx:1.12.0" implementation "androidx.appcompat:appcompat:1.6.1" @@ -87,6 +94,18 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation "androidx.work:work-runtime-ktx:2.9.0" + implementation "androidx.compose.ui:ui:1.6.2" + implementation "androidx.compose.material:material:1.6.2" + implementation 'androidx.compose.foundation:foundation:1.6.2' + implementation "androidx.compose.ui:ui-tooling-preview:1.6.2" + implementation "androidx.activity:activity-compose:1.8.2" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0" + + implementation "io.insert-koin:koin-core:3.5.3" + implementation "io.insert-koin:koin-android:3.5.3" + implementation "io.insert-koin:koin-androidx-compose:3.5.3" + implementation "com.github.satyan:sugar:1.5" implementation "com.google.code.gson:gson:2.10" @@ -113,7 +132,7 @@ dependencies { implementation "info.guardianproject.netcipher:netcipher:2.2.0-alpha" //from here: https://github.com/guardianproject/proofmode - implementation ("org.proofmode:android-libproofmode:1.0.26") { + implementation("org.proofmode:android-libproofmode:1.0.26") { transitive = false diff --git a/build.gradle b/build.gradle index 079210f0..72c6e368 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ buildscript { classpath "com.neenbedankt.gradle.plugins:android-apt:1.8" classpath "com.testdroid:gradle:2.63.1" classpath "gradle.plugin.com.browserstack.gradle:browserstack-gradle-plugin:2.3.1" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22" } } diff --git a/gradle.properties b/gradle.properties index af09e692..82e420b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true +#android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true android.nonFinalResIds=true From 48545687279119f060f7250cb13d8d42a8c6c11c Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 4 Mar 2024 18:07:36 -0800 Subject: [PATCH 02/27] fix(internetarchive): use xauthn service to simplify InternetArchive the auth keys are returned by a webservice using email/password a simple link is provided to create an account --- app/src/main/AndroidManifest.xml | 3 + .../opendasharchive/openarchive/SaveApp.kt | 11 +- .../openarchive/core/di/CoreModule.kt | 7 + .../openarchive/core/di/FeaturesModule.kt | 8 + .../openarchive/core/state/StateDispatcher.kt | 30 ++++ .../openarchive/core/state/StateListener.kt | 13 ++ .../features/internetarchive/Module.kt | 15 ++ .../domain/model/InternetArchiveAuth.kt | 6 + .../usecase/InternetArchiveLoginUseCase.kt | 4 + .../datasource/InternetArchiveRemoteSource.kt | 56 +++++++ .../mapping/InternetArchiveMapper.kt | 11 ++ .../model/InternetArchiveLoginRequest.kt | 6 + .../model/InternetArchiveLoginResponse.kt | 21 +++ .../repository/InternetArchiveRepository.kt | 20 +++ .../presentation/InternetArchiveActivity.kt | 16 ++ .../login/InternetArchiveLoginScreen.kt | 151 ++++++++++++++++++ .../login/InternetArchiveLoginState.kt | 12 ++ .../login/InternetArchiveLoginViewModel.kt | 71 ++++++++ .../openarchive/features/main/MainActivity.kt | 5 +- .../openarchive/services/SaveClient.kt | 2 +- 20 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac3552ee..56ab137e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -120,6 +120,9 @@ android:label="@string/title_activity_login" android:taskAffinity="" /> + diff --git a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt index 91196213..c44df32f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt @@ -6,8 +6,12 @@ import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.decoder.SimpleProgressiveJpegConfig import com.orm.SugarApp import info.guardianproject.netcipher.proxy.OrbotHelper +import net.opendasharchive.openarchive.core.di.coreModule +import net.opendasharchive.openarchive.core.di.featuresModule import net.opendasharchive.openarchive.util.Prefs import net.opendasharchive.openarchive.util.Theme +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin import timber.log.Timber class SaveApp : SugarApp() { @@ -19,6 +23,11 @@ class SaveApp : SugarApp() { override fun onCreate() { super.onCreate() + startKoin { + androidContext(this@SaveApp) + modules(coreModule, featuresModule) + } + val config = ImagePipelineConfig.newBuilder(this) .setProgressiveJpegConfig(SimpleProgressiveJpegConfig()) .setResizeAndRotateEnabledForNetwork(true) @@ -50,4 +59,4 @@ class SaveApp : SugarApp() { oh.init() } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt new file mode 100644 index 00000000..a9d91456 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt @@ -0,0 +1,7 @@ +package net.opendasharchive.openarchive.core.di + +import org.koin.dsl.module + +val coreModule = module { + +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt new file mode 100644 index 00000000..54cb1852 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/FeaturesModule.kt @@ -0,0 +1,8 @@ +package net.opendasharchive.openarchive.core.di + +import net.opendasharchive.openarchive.features.internetarchive.internetArchiveModule +import org.koin.dsl.module + +val featuresModule = module { + includes(internetArchiveModule) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt new file mode 100644 index 00000000..a995fea5 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt @@ -0,0 +1,30 @@ +package net.opendasharchive.openarchive.core.state + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.updateAndGet +import kotlinx.coroutines.launch + +typealias Reducer = (T, A) -> T +typealias Effect = suspend (T, A) -> Unit + +typealias Dispatch = (A) -> Unit + +class StateDispatcher( + initialState: T, + private val reducer: Reducer, + private val effects: Effect +) { + private val scope = CoroutineScope(SupervisorJob()) + + private val _state = MutableStateFlow(initialState) + val state = _state + + fun dispatch(action: A) { + val state = _state.updateAndGet { reducer(it, action) } + scope.launch { + effects(state, action) + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt new file mode 100644 index 00000000..97c89cf5 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateListener.kt @@ -0,0 +1,13 @@ +package net.opendasharchive.openarchive.core.state + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +class StateListener { + private val _actions = Channel() + val actions = _actions.receiveAsFlow() + + suspend fun send(action: T) { + _actions.send(action) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt new file mode 100644 index 00000000..d6853da7 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -0,0 +1,15 @@ +package net.opendasharchive.openarchive.features.internetarchive + +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val internetArchiveModule = module { + factory { InternetArchiveRemoteSource(get()) } + factory { InternetArchiveMapper() } + single { InternetArchiveRepository(get(), get()) } + viewModel { InternetArchiveLoginViewModel(get()) } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt new file mode 100644 index 00000000..c49806df --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt @@ -0,0 +1,6 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.model + +data class InternetArchiveAuth( + val access: String, + val secret: String, +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt new file mode 100644 index 00000000..0315feea --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt @@ -0,0 +1,4 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.usecase + +class InternetArchiveLoginUseCase { +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt new file mode 100644 index 00000000..73f355a9 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -0,0 +1,56 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource + +import android.content.Context +import com.google.gson.Gson +import kotlinx.coroutines.suspendCancellableCoroutine +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse +import net.opendasharchive.openarchive.services.SaveClient +import okhttp3.Call +import okhttp3.Callback +import okhttp3.FormBody +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +private val LOGIN_URI = "https://archive.org/services/xauthn?op=login" + +class InternetArchiveRemoteSource( + private val context: Context +) { + + suspend fun login(request: InternetArchiveLoginRequest): Result { + val client = SaveClient.get(context) + return suspendCancellableCoroutine { continuation -> + client.newCall( + Request.Builder() + .url(LOGIN_URI) + .post( + FormBody.Builder().add("email", request.email) + .add("password", request.password).build() + ) + .build() + ).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + val data = + Gson().fromJson( + response.body?.string(), + InternetArchiveLoginResponse::class.java + ) + continuation.resume(Result.success(data)) + } + + }) + + continuation.invokeOnCancellation { + client.dispatcher.cancelAll() + } + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt new file mode 100644 index 00000000..314ca2c9 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -0,0 +1,11 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping + +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse + +class InternetArchiveMapper { + + fun loginToAuth(response: InternetArchiveLoginResponse) = InternetArchiveAuth( + access = response.values.s3!!.access, secret = response.values.s3.secret + ) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt new file mode 100644 index 00000000..9e9f9889 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginRequest.kt @@ -0,0 +1,6 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.model + +data class InternetArchiveLoginRequest( + val email: String, + val password: String, +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt new file mode 100644 index 00000000..b81167eb --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt @@ -0,0 +1,21 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.model + +data class InternetArchiveLoginResponse( + val success: Boolean, + val values: Values, + val version: Int, +) { + data class Values( + val expires: String? = null, + val s3: S3? = null, + val screenname: String? = null, + val email: String? = null, + val itemname: String? = null, + val reason: String? = null + ) + + data class S3( + val access: String, + val secret: String, + ) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt new file mode 100644 index 00000000..7a595a46 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -0,0 +1,20 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository + +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest + +class InternetArchiveRepository( + private val remoteSource: InternetArchiveRemoteSource, + private val mapper: InternetArchiveMapper +) { + suspend fun login(email: String, password: String): Result = remoteSource.login( + InternetArchiveLoginRequest(email, password) + ).mapCatching { + if (it.success.not()) { + throw IllegalArgumentException(it.values.reason) + } + mapper.loginToAuth(it) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt new file mode 100644 index 00000000..23ab0820 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -0,0 +1,16 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen + +class InternetArchiveActivity: AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + InternetArchiveLoginScreen() + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt new file mode 100644 index 00000000..e4084bd2 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -0,0 +1,151 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.login + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword +import org.koin.androidx.compose.koinViewModel + +@Composable +fun InternetArchiveLoginScreen() { + val viewModel: InternetArchiveLoginViewModel = koinViewModel() + + val state by viewModel.state.collectAsState() + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = {} + ) + + LaunchedEffect(Unit) { + viewModel.effects.collect { action -> + when (action) { + is CreateLogin -> launcher.launch( + Intent( + Intent.ACTION_VIEW, + Uri.parse(CreateLogin.URI) + ) + ) + + else -> Unit + } + } + } + + InternetArchiveLoginContent(state, viewModel::dispatch) +} + +@Composable +private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispatch: Dispatch) { + + Box(modifier = Modifier.fillMaxSize()) { + Column( + Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + state.auth?.let { auth -> + Text(text = "${auth.access}:${auth.secret}", + modifier = Modifier.padding(bottom = 20.dp), + color = Color.Red) + } + + Text( + text = stringResource(id = R.string.internet_archive), + fontSize = 32.sp, + modifier = Modifier.padding(bottom = 20.dp) + ) + + TextField( + value = state.email, + onValueChange = { dispatch(UpdateEmail(it)) }, + label = { Text(stringResource(id = R.string.prompt_email)) }, + placeholder = { Text(stringResource(id = R.string.prompt_email)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + autoCorrect = false, + keyboardType = KeyboardType.Email + ), + isError = state.isEmailError + ) + + TextField( + value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, + label = { Text(stringResource(id = R.string.prompt_password)) }, + placeholder = { Text(stringResource(id = R.string.prompt_password)) }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + autoCorrect = false, + imeAction = ImeAction.Go + ), + isError = state.isPasswordError + ) + + if (state.isLoginError) { + Text( + modifier = Modifier.padding(top = 20.dp), + text = stringResource(id = R.string.error_incorrect_username_or_password), + color = MaterialTheme.colors.error + ) + } + + Button( + modifier = Modifier.padding(top = 20.dp), + onClick = { dispatch(Login) }) { + Text(stringResource(id = R.string.title_activity_login)) + } + + TextButton(onClick = { dispatch(CreateLogin) }) { + Text("Create Login") + } + } + } +} + +@Composable +@Preview +private fun InternetArchiveLoginPreview() { + InternetArchiveLoginContent( + state = InternetArchiveLoginState( + email = "user@example.org", + password = "123abc" + ) + ) {} +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt new file mode 100644 index 00000000..767e2199 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -0,0 +1,12 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.login + +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth + +data class InternetArchiveLoginState( + val email: String = "", + val password: String = "", + val isEmailError: Boolean = false, + val isPasswordError: Boolean = false, + val isLoginError: Boolean = false, + val auth: InternetArchiveAuth? = null +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt new file mode 100644 index 00000000..41006cd1 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -0,0 +1,71 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.opendasharchive.openarchive.core.state.StateDispatcher +import net.opendasharchive.openarchive.core.state.StateListener +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword + +class InternetArchiveLoginViewModel( + private val repository: InternetArchiveRepository +) : ViewModel() { + private val dispatcher = StateDispatcher(InternetArchiveLoginState(), ::reduce, ::effects) + private val listener = StateListener() + + val state = dispatcher.state + val effects = listener.actions + + private fun reduce(state: InternetArchiveLoginState, action: Action): InternetArchiveLoginState = when(action) { + is UpdateEmail -> state.copy(email = action.value) + is UpdatePassword -> state.copy(password = action.value) + is LoginError -> state.copy(isLoginError = true) + is LoginSuccess -> state.copy(auth = action.value) + else -> state + } + + private suspend fun effects(state: InternetArchiveLoginState, action: Action) { + when(action) { + is Login -> withContext(Dispatchers.IO) { + repository.login(state.email, state.password) + .onSuccess { + dispatcher.dispatch(LoginSuccess(it)) + }.onFailure { + dispatcher.dispatch(LoginError(it)) + } + } + is CreateLogin -> listener.send(action) + else -> Unit + } + } + + fun dispatch(action: Action) { + viewModelScope.launch { + dispatcher.dispatch(action) + } + } + + sealed interface Action { + data object Login: Action + + data class LoginSuccess(val value: InternetArchiveAuth): Action + + data class LoginError(val value: Throwable): Action + + data object CreateLogin: Action { + const val URI = "https://archive.org/account/signup" + } + + data class UpdateEmail(val value: String): Action + data class UpdatePassword(val value: String): Action + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt index 34e4aefc..9e97272c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt @@ -32,6 +32,7 @@ import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.folders.AddFolderActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity import net.opendasharchive.openarchive.features.media.AddMediaDialogFragment import net.opendasharchive.openarchive.features.media.Picker import net.opendasharchive.openarchive.features.media.PreviewActivity @@ -458,7 +459,9 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mBinding.spacesCard.hide() } - startActivity(Intent(this, SpaceSetupActivity::class.java)) + //startActivity(Intent(this, SpaceSetupActivity::class.java)) + + startActivity(Intent(this, InternetArchiveActivity::class.java)) } override fun getSelectedSpace(): Space? { diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt index 95802892..42912156 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt @@ -158,4 +158,4 @@ class SaveClient(context: Context) : StrongBuilderBase return sardine } } -} \ No newline at end of file +} From c364b751f26484680b1f8ad7377cceea7dfacd8e Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 13:24:26 -0800 Subject: [PATCH 03/27] fix(ia): integrating xauthn and compose flow into space setup --- .../infrastructure/client/ClientResult.kt | 29 +++++++++ .../core/presentation/StatefulViewModel.kt | 25 +++++++ .../openarchive/core/state/StateDispatcher.kt | 7 +- .../features/internetarchive/Module.kt | 2 +- .../domain/model/InternetArchive.kt | 8 +++ .../datasource/InternetArchiveRemoteSource.kt | 65 +++++++++---------- .../mapping/InternetArchiveMapper.kt | 12 +++- .../repository/InternetArchiveRepository.kt | 23 ++++--- .../presentation/InternetArchiveActivity.kt | 13 +++- .../presentation/InternetArchiveFragment.kt | 54 +++++++++++++++ .../presentation/InternetArchiveScreen.kt | 60 +++++++++++++++++ .../presentation/InternetArchiveState.kt | 7 ++ .../presentation/InternetArchiveViewModel.kt | 23 +++++++ .../login/InternetArchiveLoginScreen.kt | 22 +++---- .../login/InternetArchiveLoginState.kt | 18 ++++- .../login/InternetArchiveLoginViewModel.kt | 58 ++++++++--------- .../openarchive/features/main/MainActivity.kt | 5 +- .../features/onboarding/SpaceSetupActivity.kt | 4 +- 18 files changed, 329 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt b/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt new file mode 100644 index 00000000..9c81c173 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/infrastructure/client/ClientResult.kt @@ -0,0 +1,29 @@ +package net.opendasharchive.openarchive.core.infrastructure.client + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + suspend fun OkHttpClient.enqueueResult( + request: Request, + onResume: (Response) -> T +) = suspendCancellableCoroutine { continuation -> + newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + continuation.resume(onResume(response)) + } + }) + + continuation.invokeOnCancellation { + dispatcher.cancelAll() + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt new file mode 100644 index 00000000..1cc6258b --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/StatefulViewModel.kt @@ -0,0 +1,25 @@ +package net.opendasharchive.openarchive.core.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import net.opendasharchive.openarchive.core.state.StateDispatcher +import net.opendasharchive.openarchive.core.state.StateListener + +abstract class StatefulViewModel( + initialState: State, +) : ViewModel() { + private val dispatcher = + StateDispatcher(viewModelScope, initialState, ::reduce, ::effects) + private val listener = StateListener() + + val state = dispatcher.state + val effects = listener.actions + + abstract fun reduce(state: State, action: Action): State + + abstract suspend fun effects(state: State, action: Action) + + fun dispatch(action: Action) = dispatcher.dispatch(action) + + suspend fun send(action: Action) = listener.send(action) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt index a995fea5..a7e0758d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt @@ -1,7 +1,7 @@ package net.opendasharchive.openarchive.core.state import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch @@ -12,18 +12,17 @@ typealias Effect = suspend (T, A) -> Unit typealias Dispatch = (A) -> Unit class StateDispatcher( + private val scope: CoroutineScope, initialState: T, private val reducer: Reducer, private val effects: Effect ) { - private val scope = CoroutineScope(SupervisorJob()) - private val _state = MutableStateFlow(initialState) val state = _state fun dispatch(action: A) { val state = _state.updateAndGet { reducer(it, action) } - scope.launch { + scope.launch(Dispatchers.Default) { effects(state, action) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index d6853da7..4b8d3451 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -11,5 +11,5 @@ val internetArchiveModule = module { factory { InternetArchiveRemoteSource(get()) } factory { InternetArchiveMapper() } single { InternetArchiveRepository(get(), get()) } - viewModel { InternetArchiveLoginViewModel(get()) } + viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt new file mode 100644 index 00000000..c6c2130e --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt @@ -0,0 +1,8 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.model + +data class InternetArchive( + val username: String, + val email: String, + val expires: String, + val auth: InternetArchiveAuth +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt index 73f355a9..5f48e8fd 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -2,18 +2,14 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure. import android.content.Context import com.google.gson.Gson -import kotlinx.coroutines.suspendCancellableCoroutine +import net.opendasharchive.openarchive.core.infrastructure.client.enqueueResult +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse import net.opendasharchive.openarchive.services.SaveClient -import okhttp3.Call -import okhttp3.Callback +import net.opendasharchive.openarchive.services.internetarchive.IaConduit.Companion.ARCHIVE_API_ENDPOINT import okhttp3.FormBody import okhttp3.Request -import okhttp3.Response -import java.io.IOException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException private val LOGIN_URI = "https://archive.org/services/xauthn?op=login" @@ -21,36 +17,33 @@ class InternetArchiveRemoteSource( private val context: Context ) { - suspend fun login(request: InternetArchiveLoginRequest): Result { - val client = SaveClient.get(context) - return suspendCancellableCoroutine { continuation -> - client.newCall( - Request.Builder() - .url(LOGIN_URI) - .post( - FormBody.Builder().add("email", request.email) - .add("password", request.password).build() - ) - .build() - ).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } + private val gson = Gson() - override fun onResponse(call: Call, response: Response) { - val data = - Gson().fromJson( - response.body?.string(), - InternetArchiveLoginResponse::class.java - ) - continuation.resume(Result.success(data)) - } - - }) + suspend fun login(request: InternetArchiveLoginRequest): Result = + SaveClient.get(context).enqueueResult( + Request.Builder() + .url(LOGIN_URI) + .post( + FormBody.Builder().add("email", request.email) + .add("password", request.password).build() + ) + .build() + ) { response -> + val data = gson.fromJson( + response.body?.string(), + InternetArchiveLoginResponse::class.java + ) + Result.success(data) + } - continuation.invokeOnCancellation { - client.dispatcher.cancelAll() - } + suspend fun testConnection(auth: InternetArchiveAuth): Result = + SaveClient.get(context).enqueueResult( + Request.Builder() + .url(ARCHIVE_API_ENDPOINT) + .method("GET", null) + .addHeader("Authorization", "LOW ${auth.access}:${auth.secret}") + .build() + ) { response -> + Result.success(response.isSuccessful) } - } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index 314ca2c9..f3e4c407 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -1,11 +1,19 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse class InternetArchiveMapper { - fun loginToAuth(response: InternetArchiveLoginResponse) = InternetArchiveAuth( - access = response.values.s3!!.access, secret = response.values.s3.secret + private fun toAuth(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( + access = response.access, secret = response.secret + ) + + fun toDomain(response: InternetArchiveLoginResponse.Values) = InternetArchive( + username = response.screenname ?: response.itemname ?: "", + email = response.email ?: "", + expires = response.expires ?: "", + auth = response.s3?.let { toAuth(it) } ?: InternetArchiveAuth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 7a595a46..5dc2d6d7 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -1,6 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest @@ -9,12 +11,17 @@ class InternetArchiveRepository( private val remoteSource: InternetArchiveRemoteSource, private val mapper: InternetArchiveMapper ) { - suspend fun login(email: String, password: String): Result = remoteSource.login( - InternetArchiveLoginRequest(email, password) - ).mapCatching { - if (it.success.not()) { - throw IllegalArgumentException(it.values.reason) + suspend fun login(email: String, password: String): Result = + withContext(Dispatchers.IO) { + remoteSource.login( + InternetArchiveLoginRequest(email, password) + ).mapCatching { + if (it.success.not()) { + throw IllegalArgumentException(it.values.reason) + } + when(it.version) { + else -> mapper.toDomain(it.values) + } + } } - mapper.loginToAuth(it) - } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt index 23ab0820..a23b9471 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -3,14 +3,23 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace +import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment.Companion.ARG_VAL_NEW_SPACE class InternetArchiveActivity: AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) + setContent { - InternetArchiveLoginScreen() + if (isNewSpace) { + InternetArchiveLoginScreen(space) + } else { + InternetArchiveScreen(space) + } } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt new file mode 100644 index 00000000..038697eb --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt @@ -0,0 +1,54 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_SPACE +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_VAL_NEW_SPACE +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace +import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment + +@Deprecated("only used for backward compatibility") +class InternetArchiveFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + val (space, isNewSpace) = arguments.getSpace(Space.Type.INTERNET_ARCHIVE) + + return ComposeView(requireContext()).apply { + setContent { + if (isNewSpace) { + InternetArchiveLoginScreen(space) + } else { + InternetArchiveScreen(space) + } + } + } + } + + companion object { + + const val RESP_SAVED = "ia_fragment_resp_saved" + const val RESP_DELETED = "ia_dav_fragment_resp_deleted" + const val RESP_CANCEL = "ia_fragment_resp_cancel" + + @JvmStatic + fun newInstance(spaceId: Long) = InternetArchiveFragment().apply { + arguments = Bundle().apply { + putLong(ARG_SPACE, spaceId) + } + } + + @JvmStatic + fun newInstance() = newInstance(ARG_VAL_NEW_SPACE) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt new file mode 100644 index 00000000..9b279ce1 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -0,0 +1,60 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +@Composable +fun InternetArchiveScreen(space: Space) { + val viewModel: InternetArchiveViewModel = koinViewModel { + parametersOf(space) + } + + val state by viewModel.state.collectAsState() + + InternetArchiveContent(state, viewModel::dispatch) +} + +@Composable +private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { + Box(modifier = Modifier.fillMaxSize()) { + Column { + Text( + text = stringResource(id = R.string.prompt_email), + style = MaterialTheme.typography.caption + ) + Text( + text = state.email, + ) + Text( + text = "Username", + style = MaterialTheme.typography.caption + ) + Text( + text = state.username + ) + + Text( + text = "Expires", + style = MaterialTheme.typography.caption + ) + + Text( + text = state.expires + ) + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt new file mode 100644 index 00000000..7c0e44cf --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt @@ -0,0 +1,7 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +data class InternetArchiveState( + val username: String = "", + val email: String = "", + val expires: String= "", +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt new file mode 100644 index 00000000..3cf41cfa --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt @@ -0,0 +1,23 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation + +import net.opendasharchive.openarchive.core.presentation.StatefulViewModel +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action + +class InternetArchiveViewModel(private val space: Space) : + StatefulViewModel(InternetArchiveState()) { + + override fun reduce(state: InternetArchiveState, action: Action) = when (action) { + else -> state + } + + override suspend fun effects(state: InternetArchiveState, action: Action) { + when (action) { + else -> Unit + } + } + + sealed interface Action { + + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index e4084bd2..ff6c4376 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -1,6 +1,5 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import android.app.Activity import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult @@ -21,8 +20,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -32,17 +29,20 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf @Composable -fun InternetArchiveLoginScreen() { - val viewModel: InternetArchiveLoginViewModel = koinViewModel() +fun InternetArchiveLoginScreen(space: Space) { + val viewModel: InternetArchiveLoginViewModel = koinViewModel { + parametersOf(space) + } val state by viewModel.state.collectAsState() @@ -78,12 +78,6 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat horizontalAlignment = Alignment.CenterHorizontally ) { - state.auth?.let { auth -> - Text(text = "${auth.access}:${auth.secret}", - modifier = Modifier.padding(bottom = 20.dp), - color = Color.Red) - } - Text( text = stringResource(id = R.string.internet_archive), fontSize = 32.sp, @@ -132,7 +126,9 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat Text(stringResource(id = R.string.title_activity_login)) } - TextButton(onClick = { dispatch(CreateLogin) }) { + TextButton( + modifier = Modifier.padding(top = 10.dp), + onClick = { dispatch(CreateLogin) }) { Text("Create Login") } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index 767e2199..e1119507 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,6 +1,7 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import android.os.Bundle +import net.opendasharchive.openarchive.db.Space data class InternetArchiveLoginState( val email: String = "", @@ -8,5 +9,18 @@ data class InternetArchiveLoginState( val isEmailError: Boolean = false, val isPasswordError: Boolean = false, val isLoginError: Boolean = false, - val auth: InternetArchiveAuth? = null ) + +const val ARG_VAL_NEW_SPACE = -1L +const val ARG_SPACE = "space" +fun Bundle?.getSpace(type: Space.Type): Pair { + val mSpaceId = this?.getLong(ARG_SPACE, ARG_VAL_NEW_SPACE) ?: ARG_VAL_NEW_SPACE + + val isNewSpace = ARG_VAL_NEW_SPACE == mSpaceId + + return if (isNewSpace) { + Pair(Space(type), true) + } else { + Space.get(mSpaceId)?.let { Pair(it, false) } ?: Pair(Space(type), true) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index 41006cd1..14e7ab76 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -1,14 +1,12 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import net.opendasharchive.openarchive.core.state.StateDispatcher -import net.opendasharchive.openarchive.core.state.StateListener -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.core.presentation.StatefulViewModel +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError @@ -17,55 +15,51 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.log import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword class InternetArchiveLoginViewModel( - private val repository: InternetArchiveRepository -) : ViewModel() { - private val dispatcher = StateDispatcher(InternetArchiveLoginState(), ::reduce, ::effects) - private val listener = StateListener() + private val repository: InternetArchiveRepository, + private val space: Space, +) : StatefulViewModel(InternetArchiveLoginState()) { - val state = dispatcher.state - val effects = listener.actions - - private fun reduce(state: InternetArchiveLoginState, action: Action): InternetArchiveLoginState = when(action) { + override fun reduce( + state: InternetArchiveLoginState, + action: Action + ): InternetArchiveLoginState = when (action) { is UpdateEmail -> state.copy(email = action.value) is UpdatePassword -> state.copy(password = action.value) is LoginError -> state.copy(isLoginError = true) - is LoginSuccess -> state.copy(auth = action.value) else -> state } - private suspend fun effects(state: InternetArchiveLoginState, action: Action) { - when(action) { + override suspend fun effects(state: InternetArchiveLoginState, action: Action) { + when (action) { is Login -> withContext(Dispatchers.IO) { repository.login(state.email, state.password) .onSuccess { - dispatcher.dispatch(LoginSuccess(it)) + space.username = it.auth.access + space.password = it.auth.secret + space.save() + send(LoginSuccess(it)) }.onFailure { - dispatcher.dispatch(LoginError(it)) + dispatch(LoginError(it)) } } - is CreateLogin -> listener.send(action) - else -> Unit - } - } - fun dispatch(action: Action) { - viewModelScope.launch { - dispatcher.dispatch(action) + is CreateLogin -> send(action) + else -> Unit } } sealed interface Action { - data object Login: Action + data object Login : Action - data class LoginSuccess(val value: InternetArchiveAuth): Action + data class LoginSuccess(val value: InternetArchive) : Action - data class LoginError(val value: Throwable): Action + data class LoginError(val value: Throwable) : Action - data object CreateLogin: Action { + data object CreateLogin : Action { const val URI = "https://archive.org/account/signup" } - data class UpdateEmail(val value: String): Action - data class UpdatePassword(val value: String): Action + data class UpdateEmail(val value: String) : Action + data class UpdatePassword(val value: String) : Action } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt index 9e97272c..34e4aefc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt @@ -32,7 +32,6 @@ import net.opendasharchive.openarchive.db.Project import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.folders.AddFolderActivity -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity import net.opendasharchive.openarchive.features.media.AddMediaDialogFragment import net.opendasharchive.openarchive.features.media.Picker import net.opendasharchive.openarchive.features.media.PreviewActivity @@ -459,9 +458,7 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener mBinding.spacesCard.hide() } - //startActivity(Intent(this, SpaceSetupActivity::class.java)) - - startActivity(Intent(this, InternetArchiveActivity::class.java)) + startActivity(Intent(this, SpaceSetupActivity::class.java)) } override fun getSelectedSpace(): Space? { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt index 16336697..fd11b2e0 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/onboarding/SpaceSetupActivity.kt @@ -10,7 +10,7 @@ import net.opendasharchive.openarchive.features.settings.SpaceSetupFragment import net.opendasharchive.openarchive.features.settings.SpaceSetupSuccessFragment import net.opendasharchive.openarchive.services.dropbox.DropboxFragment import net.opendasharchive.openarchive.services.gdrive.GDriveFragment -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveFragment import net.opendasharchive.openarchive.services.internetarchive.Util import net.opendasharchive.openarchive.services.webdav.WebDavFragment @@ -242,4 +242,4 @@ class SpaceSetupActivity : BaseActivity() { Util.setBackgroundTint(mBinding.progressBlock.bar2, R.color.colorSpaceSetupProgressOn) Util.setBackgroundTint(mBinding.progressBlock.dot3, R.color.colorSpaceSetupProgressOn) } -} \ No newline at end of file +} From d38dceb08f255dc6ca6a3d6be577dc7a7a7e32e4 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 15:17:10 -0800 Subject: [PATCH 04/27] fix(fastlane): add versioning plugin and ability to do a manual release build --- Gemfile | 10 ++ Gemfile.lock | 222 ++++++++++++++++++++++++++++++++++++++++++++ fastlane/Fastfile | 70 ++++++++++---- fastlane/Pluginfile | 1 + 4 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Pluginfile diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..b734015f --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +source "https://rubygems.org" + +gem 'fastlane' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..e9b03fb3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,222 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.895.0) + aws-sdk-core (3.191.3) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.77.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.143.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.109.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.3.0) + fastlane (2.219.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-android_version_manager (0.4.1) + semantic (~> 1.6.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.7.1) + jwt (2.8.1) + base64 + mini_magick (4.12.0) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.0) + nanaimo (0.3.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.4.0) + os (1.1.4) + plist (3.7.1) + public_suffix (5.0.4) + rake (13.1.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.3) + semantic (1.6.1) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.5.0) + word_wrap (1.0.0) + xcodeproj (1.24.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + x86_64-linux + +DEPENDENCIES + fastlane + fastlane-plugin-android_version_manager + +BUNDLED WITH + 2.5.6 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ab92687c..b3938036 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -6,30 +6,64 @@ platform :android do gradle(task: "test") end + desc "Create a release build for manual deployment" + lane :release do + gradle( + task: "assemble", + build_type: "release", + properties: { + "android.injected.signing.store.file" => ENV["FASTLANE_KEYSTORE_FILE"], + "android.injected.signing.store.password" => ENV["FASTLANE_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["FASTLANE_KEY_ALIAS"], + "android.injected.signing.key.password" => ENV["FASTLANE_KEY_PASSWORD"], + } + ) + + send_progress_message("Copying APK to current folder") + copy_artifacts( + artifacts: [lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]], + target_path: ENV["PWD"] + ) + end + + desc "Increments the version code" + lane :increment_version_code do + android_increment_version_code( + app_project_dir: "./app" + ) + end + + desc "Increments the version name" + lane :increment_version_name do + android_increment_version_name( + app_project_dir: "./app" + ) + end + desc "Submit a new Internal Build" lane :internal do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Internal track :rocket:") + send_progress_message("Uploading To Internal track") upload_to_play_store(track: "internal") end desc "Submit a new Alpha Build" lane :alpha do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Alpha track :rocket:") + send_progress_message("Uploading To Alpha track") upload_to_play_store(track: "alpha") end desc "Submit a new Beta Build" lane :beta do - send_progress_message("Build Started :rocket:") + send_progress_message("Build Started 🚀") gradle(task: "clean assembleRelease") - send_progress_message("Uploading To Beta track :rocket:") + send_progress_message("Uploading To Beta track") upload_to_play_store(track: "beta") end @@ -53,18 +87,14 @@ def on_error(exception) end after_all do |lane| - file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") - send_message "Successfully deployed new App Update! :champagne:" - default_payloads = [ - :git_branch, - :last_git_commit_hash, - :last_git_commit_message - ] - payload = { - "Build Date" => Time.new.to_s, - "APK" => file_name - } - send_message file_name - send_message "#{default_payloads}" - send_message "#{payload}" + if lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH] + file_name = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH].gsub(/\/.*\//,"") + send_message "Successfully deployed new App Update!" + payload = { + "Build Date" => Time.new.to_s, + "APK" => file_name + } + send_message file_name + send_message "#{payload}" + end end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 00000000..2866db63 --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1 @@ +gem 'fastlane-plugin-android_version_manager' From 1a666279898f245a52a87c6374ef1b921ef7cac5 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 18:24:23 -0800 Subject: [PATCH 05/27] fix(ia): add result handlers, styling, alerts, strings --- .../presentation/InternetArchiveActivity.kt | 19 ++- .../presentation/InternetArchiveFragment.kt | 15 ++- .../presentation/InternetArchiveScreen.kt | 112 +++++++++++++++++- .../presentation/InternetArchiveViewModel.kt | 12 +- .../components/InternetArchiveHeader.kt | 62 ++++++++++ .../login/InternetArchiveLoginScreen.kt | 109 +++++++++++++---- .../login/InternetArchiveLoginViewModel.kt | 8 +- app/src/main/res/values/strings.xml | 1 + 8 files changed, 300 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt index a23b9471..a8c98414 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -1,13 +1,13 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation +import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment.Companion.ARG_VAL_NEW_SPACE +import net.opendasharchive.openarchive.features.main.MainActivity class InternetArchiveActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -16,10 +16,21 @@ class InternetArchiveActivity: AppCompatActivity() { setContent { if (isNewSpace) { - InternetArchiveLoginScreen(space) + InternetArchiveLoginScreen(space) { + finish(it) + } } else { - InternetArchiveScreen(space) + InternetArchiveScreen(space) { + finish(it) + } } } } + + private fun finish(result: String) { + when(result) { + RESP_SAVED -> startActivity(Intent(this, MainActivity::class.java)) + RESP_CANCEL -> Space.navigate(this) + } + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt index 038697eb..826ea755 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt @@ -5,13 +5,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_SPACE import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_VAL_NEW_SPACE import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveFragment + +const val RESP_SAVED = "ia_fragment_resp_saved" +const val RESP_DELETED = "ia_dav_fragment_resp_deleted" +const val RESP_CANCEL = "ia_fragment_resp_cancel" @Deprecated("only used for backward compatibility") class InternetArchiveFragment : Fragment() { @@ -27,9 +32,13 @@ class InternetArchiveFragment : Fragment() { return ComposeView(requireContext()).apply { setContent { if (isNewSpace) { - InternetArchiveLoginScreen(space) + InternetArchiveLoginScreen(space) { result -> + setFragmentResult(result, bundleOf()) + } } else { - InternetArchiveScreen(space) + InternetArchiveScreen(space) { result -> + setFragmentResult(result, bundleOf()) + } } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 9b279ce1..8d74aff7 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -3,58 +3,162 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState 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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf +import java.util.Date @Composable -fun InternetArchiveScreen(space: Space) { +fun InternetArchiveScreen(space: Space, onResult: (String) -> Unit) { val viewModel: InternetArchiveViewModel = koinViewModel { parametersOf(space) } val state by viewModel.state.collectAsState() + LaunchedEffect(Unit) { + viewModel.effects.collect { action -> + when (action) { + is Action.Remove -> { + onResult(RESP_DELETED) + } + is Action.Cancel -> { + onResult(RESP_CANCEL) + } + } + } + } + InternetArchiveContent(state, viewModel::dispatch) } @Composable private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { - Box(modifier = Modifier.fillMaxSize()) { + + var isRemoving: Boolean by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + ) { + Column { + + InternetArchiveHeader() + Text( + modifier = Modifier.padding(top = 12.dp), text = stringResource(id = R.string.prompt_email), style = MaterialTheme.typography.caption ) Text( text = state.email, + fontSize = 18.sp ) Text( + modifier = Modifier.padding(top = 12.dp), text = "Username", style = MaterialTheme.typography.caption ) Text( - text = state.username + text = state.username, + fontSize = 18.sp ) Text( + modifier = Modifier.padding(top = 12.dp), text = "Expires", style = MaterialTheme.typography.caption ) Text( - text = state.expires + text = state.expires, + fontSize = 18.sp ) + + Button( + modifier = Modifier + .padding(top = 12.dp) + .align(Alignment.CenterHorizontally), + onClick = { + isRemoving = true + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) + ) { + Text(stringResource(id = R.string.menu_delete)) + } } } + + if (isRemoving) { + RemoveInternetArchiveDialog(isRemoving = isRemoving, onDismiss = { isRemoving = false }) { + dispatch(Action.Remove) + } + } +} + +@Composable +private fun RemoveInternetArchiveDialog(isRemoving: Boolean, onDismiss: () -> Unit, onRemove: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(id = R.string.remove_from_app))}, + text = { Text(stringResource(id = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app))}, + dismissButton = { + OutlinedButton( + onClick = onDismiss + ) { + Text(stringResource(id = R.string.action_cancel)) + } + }, confirmButton = { + Button( + onClick = onRemove, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) + ) { + Text(stringResource(id = R.string.remove)) + } + }) +} + +@Composable +@Preview(showBackground = true) +private fun InternetArchiveScreenPreview() { + InternetArchiveContent( + state = InternetArchiveState( + email = "abc@example.com", + username = "@abc_name", + expires = Date().toString() + ) + ) {} +} + +@Composable +@Preview(showBackground = true) +private fun RemoveInternetArchiveDialogPreview() { + RemoveInternetArchiveDialog(isRemoving = true, onDismiss = { }) { + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt index 3cf41cfa..3e3d906b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt @@ -7,17 +7,21 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.Int class InternetArchiveViewModel(private val space: Space) : StatefulViewModel(InternetArchiveState()) { - override fun reduce(state: InternetArchiveState, action: Action) = when (action) { - else -> state - } + override fun reduce(state: InternetArchiveState, action: Action) = state override suspend fun effects(state: InternetArchiveState, action: Action) { when (action) { - else -> Unit + is Action.Remove -> { + space.delete() + send(action) + } + is Action.Cancel -> send(action) } } sealed interface Action { + data object Remove : Action + data object Cancel : Action } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt new file mode 100644 index 00000000..2c6ddb67 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt @@ -0,0 +1,62 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.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.graphics.ColorFilter.Companion.tint +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import net.opendasharchive.openarchive.R + +@Composable +fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier + .size(48.dp) + .background( + color = colorResource(id = R.color.colorBackgroundSpaceIcon), + shape = CircleShape + ).clip(CircleShape)) { + Image( + modifier = Modifier.matchParentSize().padding(12.dp), + painter = painterResource(id = R.drawable.ic_internet_archive), + contentDescription = stringResource( + id = R.string.internet_archive + ), + colorFilter = tint(colorResource(id = R.color.colorPrimary)) + ) + } + Column(modifier = Modifier.padding(start = 8.dp)) { + Text( + text = stringResource(id = R.string.internet_archive), + fontSize = titleSize, + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(id = R.string.internet_archive_description) + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun InternetArchiveHeaderPreview() { + InternetArchiveHeader() +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index ff6c4376..117fc351 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -4,32 +4,47 @@ import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource +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.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_CANCEL +import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_SAVED +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login @@ -39,7 +54,7 @@ import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @Composable -fun InternetArchiveLoginScreen(space: Space) { +fun InternetArchiveLoginScreen(space: Space, onResult: (String) -> Unit) { val viewModel: InternetArchiveLoginViewModel = koinViewModel { parametersOf(space) } @@ -61,6 +76,10 @@ fun InternetArchiveLoginScreen(space: Space) { ) ) + is Action.Cancel -> onResult(RESP_CANCEL) + + is Action.LoginSuccess -> onResult(RESP_SAVED) + else -> Unit } } @@ -70,38 +89,54 @@ fun InternetArchiveLoginScreen(space: Space) { } @Composable -private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispatch: Dispatch) { +private fun InternetArchiveLoginContent( + state: InternetArchiveLoginState, + dispatch: Dispatch +) { + + LaunchedEffect(state.isLoginError) { + while (state.isLoginError) { + delay(3000) + dispatch(Action.ErrorFade) + } + } - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + ) { Column( Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = stringResource(id = R.string.internet_archive), - fontSize = 32.sp, - modifier = Modifier.padding(bottom = 20.dp) + InternetArchiveHeader( + modifier = Modifier.padding(bottom = 24.dp), + titleSize = 32.sp ) TextField( value = state.email, onValueChange = { dispatch(UpdateEmail(it)) }, label = { Text(stringResource(id = R.string.prompt_email)) }, - placeholder = { Text(stringResource(id = R.string.prompt_email)) }, singleLine = true, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, autoCorrect = false, keyboardType = KeyboardType.Email ), - isError = state.isEmailError + isError = state.isEmailError, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = colorResource(id = R.color.colorPrimary) + ) ) + Spacer(Modifier.height(12.dp)) + TextField( value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, label = { Text(stringResource(id = R.string.prompt_password)) }, - placeholder = { Text(stringResource(id = R.string.prompt_password)) }, singleLine = true, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( @@ -109,10 +144,13 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat autoCorrect = false, imeAction = ImeAction.Go ), - isError = state.isPasswordError + isError = state.isPasswordError, + colors = TextFieldDefaults.textFieldColors( + focusedIndicatorColor = colorResource(id = R.color.colorPrimary) + ) ) - if (state.isLoginError) { + AnimatedVisibility(visible = state.isLoginError) { Text( modifier = Modifier.padding(top = 20.dp), text = stringResource(id = R.string.error_incorrect_username_or_password), @@ -120,28 +158,57 @@ private fun InternetArchiveLoginContent(state: InternetArchiveLoginState, dispat ) } - Button( - modifier = Modifier.padding(top = 20.dp), - onClick = { dispatch(Login) }) { - Text(stringResource(id = R.string.title_activity_login)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + OutlinedButton( + colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colors.onSurface), + onClick = { dispatch(Action.Cancel) } + ) { + Text(stringResource(id = R.string.action_cancel)) + } + Button( + onClick = { dispatch(Login) }, + colors = ButtonDefaults.buttonColors( + backgroundColor = colorResource(id = R.color.colorPrimary), + contentColor = colorResource(id = R.color.colorBackground) + ) + ) { + Text( + text = stringResource(id = R.string.title_activity_login), + fontSize = 18.sp, + ) + } } - TextButton( + Row( modifier = Modifier.padding(top = 10.dp), - onClick = { dispatch(CreateLogin) }) { - Text("Create Login") + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "No account?" + ) + TextButton( + colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), + onClick = { dispatch(CreateLogin) }) { + Text(text = "Create Login", fontSize = 16.sp, fontWeight = FontWeight.Black) + } } } } } @Composable -@Preview +@Preview(showBackground = true) private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( email = "user@example.org", - password = "123abc" + password = "abc123" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index 14e7ab76..bbe08a47 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -26,6 +26,7 @@ class InternetArchiveLoginViewModel( is UpdateEmail -> state.copy(email = action.value) is UpdatePassword -> state.copy(password = action.value) is LoginError -> state.copy(isLoginError = true) + is Action.ErrorFade -> state.copy(isLoginError = false) else -> state } @@ -42,8 +43,7 @@ class InternetArchiveLoginViewModel( dispatch(LoginError(it)) } } - - is CreateLogin -> send(action) + is CreateLogin, is Action.Cancel -> send(action) else -> Unit } } @@ -51,10 +51,14 @@ class InternetArchiveLoginViewModel( sealed interface Action { data object Login : Action + data object Cancel : Action + data class LoginSuccess(val value: InternetArchive) : Action data class LoginError(val value: Throwable) : Action + data object ErrorFade : Action + data object CreateLogin : Action { const val URI = "https://archive.org/account/signup" } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6813a14a..a46ca1d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Internet Archive + Upload and preserve media in a digital library Google Drive Google Drive™ Upload to Google Drive From 3bc577507de6e0b7b4acce7662c5d580c3efdc6d Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 18:34:53 -0800 Subject: [PATCH 06/27] fix(ia): login text field colors --- .../login/InternetArchiveLoginScreen.kt | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index 117fc351..cd8a475a 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -119,7 +119,12 @@ private fun InternetArchiveLoginContent( TextField( value = state.email, onValueChange = { dispatch(UpdateEmail(it)) }, - label = { Text(stringResource(id = R.string.prompt_email)) }, + label = { + Text( + text = stringResource(id = R.string.prompt_email), + color = colorResource(id = R.color.colorPrimary) + ) + }, singleLine = true, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, @@ -127,16 +132,25 @@ private fun InternetArchiveLoginContent( keyboardType = KeyboardType.Email ), isError = state.isEmailError, - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = colorResource(id = R.color.colorPrimary) - ) + colors = colorResource(id = R.color.colorPrimary).let { + TextFieldDefaults.textFieldColors( + focusedIndicatorColor = it, + focusedLabelColor = it, + cursorColor = it + ) + } ) Spacer(Modifier.height(12.dp)) TextField( value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, - label = { Text(stringResource(id = R.string.prompt_password)) }, + label = { + Text( + stringResource(id = R.string.prompt_password), + color = colorResource(id = R.color.colorPrimary) + ) + }, singleLine = true, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( @@ -145,9 +159,13 @@ private fun InternetArchiveLoginContent( imeAction = ImeAction.Go ), isError = state.isPasswordError, - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = colorResource(id = R.color.colorPrimary) - ) + colors = colorResource(id = R.color.colorPrimary).let { + TextFieldDefaults.textFieldColors( + focusedIndicatorColor = it, + focusedLabelColor = it, + cursorColor = it + ) + } ) AnimatedVisibility(visible = state.isLoginError) { From 3fca8d38b9c4f1531af7e71e867d1123b814665e Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Tue, 5 Mar 2024 18:35:52 -0800 Subject: [PATCH 07/27] fix(main): ensure settings in adapter is always in line with button order --- .../opendasharchive/openarchive/features/main/ProjectAdapter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt index 35e1d99c..8906074f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/ProjectAdapter.kt @@ -19,7 +19,7 @@ class ProjectAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : } val settingsIndex: Int - get() = projects.size + get() = max(1, projects.size) fun updateData(projects: List) { this.projects = projects From bb2803a4f8fa1f5e56d53f25e08969634d0e5a0c Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 00:29:45 -0800 Subject: [PATCH 08/27] fix(ia): move login logic to use case with testing the connection consolidated bundles, activity and fragment code. should ultimately be not necessary. added fadein/out of login errors use a spinner and disable text fields while logging in --- .../openarchive/core/state/StateDispatcher.kt | 3 +- .../features/internetarchive/Module.kt | 11 +++++- .../usecase/InternetArchiveLoginUseCase.kt | 13 ++++++- .../datasource/InternetArchiveRemoteSource.kt | 8 ++-- .../mapping/InternetArchiveMapper.kt | 6 +-- .../model/UnauthenticatedException.kt | 3 ++ .../repository/InternetArchiveRepository.kt | 16 +++++--- .../presentation/InternetArchiveActivity.kt | 30 +++++++++++--- .../presentation/InternetArchiveFragment.kt | 39 +++++++++++-------- .../presentation/InternetArchiveScreen.kt | 18 ++++----- .../presentation/InternetArchiveState.kt | 1 + .../presentation/components/BundleExt.kt | 38 ++++++++++++++++++ .../login/InternetArchiveLoginScreen.kt | 37 ++++++++++++------ .../login/InternetArchiveLoginState.kt | 18 +-------- .../login/InternetArchiveLoginViewModel.kt | 38 ++++++++++-------- gradle.properties | 1 - 16 files changed, 182 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt index a7e0758d..2aac0d91 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/state/StateDispatcher.kt @@ -3,6 +3,7 @@ package net.opendasharchive.openarchive.core.state import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ class StateDispatcher( private val effects: Effect ) { private val _state = MutableStateFlow(initialState) - val state = _state + val state = _state.asStateFlow() fun dispatch(action: A) { val state = _state.updateAndGet { reducer(it, action) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 4b8d3451..1eea9aaa 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -1,5 +1,7 @@ package net.opendasharchive.openarchive.features.internetarchive +import com.google.gson.FieldNamingPolicy +import com.google.gson.Gson import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository @@ -8,8 +10,13 @@ import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val internetArchiveModule = module { - factory { InternetArchiveRemoteSource(get()) } + single { + Gson().newBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create() + } + factory { InternetArchiveRemoteSource(get(), get()) } factory { InternetArchiveMapper() } - single { InternetArchiveRepository(get(), get()) } + factory { InternetArchiveRepository(get(), get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt index 0315feea..e2c9bc1c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt @@ -1,4 +1,15 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.usecase -class InternetArchiveLoginUseCase { +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository + +class InternetArchiveLoginUseCase( + private val repository: InternetArchiveRepository +) { + + suspend operator fun invoke(email: String, password: String): Result = + repository.login(email, password).mapCatching { response -> + repository.testConnection(response.auth).getOrThrow() + response + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt index 5f48e8fd..47f0bf26 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -11,14 +11,12 @@ import net.opendasharchive.openarchive.services.internetarchive.IaConduit.Compan import okhttp3.FormBody import okhttp3.Request -private val LOGIN_URI = "https://archive.org/services/xauthn?op=login" +private const val LOGIN_URI = "https://archive.org/services/xauthn?op=login" class InternetArchiveRemoteSource( - private val context: Context + private val context: Context, + private val gson: Gson ) { - - private val gson = Gson() - suspend fun login(request: InternetArchiveLoginRequest): Result = SaveClient.get(context).enqueueResult( Request.Builder() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index f3e4c407..95541e18 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -6,14 +6,14 @@ import net.opendasharchive.openarchive.features.internetarchive.infrastructure.m class InternetArchiveMapper { - private fun toAuth(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( + private operator fun invoke(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( access = response.access, secret = response.secret ) - fun toDomain(response: InternetArchiveLoginResponse.Values) = InternetArchive( + operator fun invoke(response: InternetArchiveLoginResponse.Values) = InternetArchive( username = response.screenname ?: response.itemname ?: "", email = response.email ?: "", expires = response.expires ?: "", - auth = response.s3?.let { toAuth(it) } ?: InternetArchiveAuth("", "") + auth = response.s3?.let { invoke(it) } ?: InternetArchiveAuth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt new file mode 100644 index 00000000..a803cb4b --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/UnauthenticatedException.kt @@ -0,0 +1,3 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.model + +class UnauthenticatedException : RuntimeException() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 5dc2d6d7..41435d32 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -3,9 +3,11 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure. import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.UnauthenticatedException class InternetArchiveRepository( private val remoteSource: InternetArchiveRemoteSource, @@ -15,13 +17,17 @@ class InternetArchiveRepository( withContext(Dispatchers.IO) { remoteSource.login( InternetArchiveLoginRequest(email, password) - ).mapCatching { - if (it.success.not()) { - throw IllegalArgumentException(it.values.reason) + ).mapCatching { response -> + if (response.success.not()) { + throw IllegalArgumentException(response.values.reason) } - when(it.version) { - else -> mapper.toDomain(it.values) + when(response.version) { + else -> mapper(response.values) } } } + + suspend fun testConnection(auth: InternetArchiveAuth): Result = withContext(Dispatchers.IO) { + remoteSource.testConnection(auth).mapCatching { if (!it) throw UnauthenticatedException() } + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt index a8c98414..ff9137ec 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -1,15 +1,19 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation +import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import net.opendasharchive.openarchive.CleanInsightsManager import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace import net.opendasharchive.openarchive.features.main.MainActivity -class InternetArchiveActivity: AppCompatActivity() { +@Deprecated("use jetpack compose") +class InternetArchiveActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) @@ -27,10 +31,24 @@ class InternetArchiveActivity: AppCompatActivity() { } } - private fun finish(result: String) { - when(result) { - RESP_SAVED -> startActivity(Intent(this, MainActivity::class.java)) - RESP_CANCEL -> Space.navigate(this) + private fun finish(result: IAResult) { + when (result) { + IAResult.Saved -> { + startActivity(Intent(this, MainActivity::class.java)) + measureNewBackend(Space.Type.INTERNET_ARCHIVE) + } + IAResult.Cancelled -> Space.navigate(this) + else -> Unit } } } + +fun Activity.measureNewBackend(type: Space.Type) { + CleanInsightsManager.getConsent(this) { + CleanInsightsManager.measureEvent( + "backend", + "new", + type.friendlyName + ) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt index 826ea755..2c7a11ba 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt @@ -9,14 +9,11 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_SPACE -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.ARG_VAL_NEW_SPACE +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithNewSpace +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithSpaceId +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.getSpace - -const val RESP_SAVED = "ia_fragment_resp_saved" -const val RESP_DELETED = "ia_dav_fragment_resp_deleted" -const val RESP_CANCEL = "ia_fragment_resp_cancel" @Deprecated("only used for backward compatibility") class InternetArchiveFragment : Fragment() { @@ -33,31 +30,39 @@ class InternetArchiveFragment : Fragment() { setContent { if (isNewSpace) { InternetArchiveLoginScreen(space) { result -> - setFragmentResult(result, bundleOf()) + finish(result) } } else { InternetArchiveScreen(space) { result -> - setFragmentResult(result, bundleOf()) + finish(result) } } } } } + private fun finish(result: IAResult) { + setFragmentResult(result.value, bundleOf()) + + if (result == IAResult.Saved) { + activity?.measureNewBackend(Space.Type.INTERNET_ARCHIVE) + } + } + companion object { - const val RESP_SAVED = "ia_fragment_resp_saved" - const val RESP_DELETED = "ia_dav_fragment_resp_deleted" - const val RESP_CANCEL = "ia_fragment_resp_cancel" + val RESP_SAVED = IAResult.Saved.value + val RESP_CANCEL = IAResult.Cancelled.value @JvmStatic - fun newInstance(spaceId: Long) = InternetArchiveFragment().apply { - arguments = Bundle().apply { - putLong(ARG_SPACE, spaceId) - } + fun newInstance(args: Bundle) = InternetArchiveFragment().apply { + arguments = args } @JvmStatic - fun newInstance() = newInstance(ARG_VAL_NEW_SPACE) + fun newInstance(spaceId: Long) = newInstance(args = bundleWithSpaceId(spaceId)) + + @JvmStatic + fun newInstance() = newInstance(args = bundleWithNewSpace()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 8d74aff7..7bab2d5d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -27,13 +27,14 @@ import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf import java.util.Date @Composable -fun InternetArchiveScreen(space: Space, onResult: (String) -> Unit) { +fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { val viewModel: InternetArchiveViewModel = koinViewModel { parametersOf(space) } @@ -43,12 +44,8 @@ fun InternetArchiveScreen(space: Space, onResult: (String) -> Unit) { LaunchedEffect(Unit) { viewModel.effects.collect { action -> when (action) { - is Action.Remove -> { - onResult(RESP_DELETED) - } - is Action.Cancel -> { - onResult(RESP_CANCEL) - } + is Action.Remove -> onResult(IAResult.Deleted) + is Action.Cancel -> onResult(IAResult.Cancelled) } } } @@ -116,14 +113,14 @@ private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispat } if (isRemoving) { - RemoveInternetArchiveDialog(isRemoving = isRemoving, onDismiss = { isRemoving = false }) { + RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { dispatch(Action.Remove) } } } @Composable -private fun RemoveInternetArchiveDialog(isRemoving: Boolean, onDismiss: () -> Unit, onRemove: () -> Unit) { +private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, title = { Text(stringResource(id = R.string.remove_from_app))}, @@ -159,6 +156,5 @@ private fun InternetArchiveScreenPreview() { @Composable @Preview(showBackground = true) private fun RemoveInternetArchiveDialogPreview() { - RemoveInternetArchiveDialog(isRemoving = true, onDismiss = { }) { - } + RemoveInternetArchiveDialog(onDismiss = { }) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt index 7c0e44cf..dcffbe1d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt @@ -5,3 +5,4 @@ data class InternetArchiveState( val email: String = "", val expires: String= "", ) + diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt new file mode 100644 index 00000000..c2321afc --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/BundleExt.kt @@ -0,0 +1,38 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.components + +import android.os.Bundle +import androidx.core.os.bundleOf +import net.opendasharchive.openarchive.db.Space + +@Deprecated("only for use with fragments and activities") +private const val ARG_VAL_NEW_SPACE = -1L + +@Deprecated("only for use with fragments and activities") +private const val ARG_SPACE = "space" + +@Deprecated("only for use with fragments and activities") +enum class IAResult( + @Deprecated("only for use with fragments and activities") + val value: String +) { + Saved("ia_fragment_resp_saved"), Deleted("ia_fragment_resp_deleted"), Cancelled("ia_fragment_resp_cancel"), +} + +@Deprecated("only for use with fragments and activities") +fun bundleWithSpaceId(spaceId: Long) = bundleOf(ARG_SPACE to spaceId) + +@Deprecated("only for use with fragments and activities") +fun bundleWithNewSpace() = bundleOf(ARG_SPACE to ARG_VAL_NEW_SPACE) + +@Deprecated("only for use with fragments and activities") +fun Bundle?.getSpace(type: Space.Type): Pair { + val mSpaceId = this?.getLong(ARG_SPACE, ARG_VAL_NEW_SPACE) ?: ARG_VAL_NEW_SPACE + + val isNewSpace = ARG_VAL_NEW_SPACE == mSpaceId + + return if (isNewSpace) { + Pair(Space(type), true) + } else { + Space.get(mSpaceId)?.let { Pair(it, false) } ?: Pair(Space(type), true) + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index cd8a475a..f9ffbea9 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -5,6 +5,8 @@ import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedButton import androidx.compose.material.Text @@ -42,8 +45,7 @@ import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_CANCEL -import net.opendasharchive.openarchive.features.internetarchive.presentation.RESP_SAVED +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin @@ -54,7 +56,7 @@ import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @Composable -fun InternetArchiveLoginScreen(space: Space, onResult: (String) -> Unit) { +fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit) { val viewModel: InternetArchiveLoginViewModel = koinViewModel { parametersOf(space) } @@ -76,9 +78,9 @@ fun InternetArchiveLoginScreen(space: Space, onResult: (String) -> Unit) { ) ) - is Action.Cancel -> onResult(RESP_CANCEL) + is Action.Cancel -> onResult(IAResult.Cancelled) - is Action.LoginSuccess -> onResult(RESP_SAVED) + is Action.LoginSuccess -> onResult(IAResult.Saved) else -> Unit } @@ -118,6 +120,7 @@ private fun InternetArchiveLoginContent( TextField( value = state.email, + enabled = !state.isBusy, onValueChange = { dispatch(UpdateEmail(it)) }, label = { Text( @@ -144,7 +147,9 @@ private fun InternetArchiveLoginContent( Spacer(Modifier.height(12.dp)) TextField( - value = state.password, onValueChange = { dispatch(UpdatePassword(it)) }, + value = state.password, + enabled = !state.isBusy, + onValueChange = { dispatch(UpdatePassword(it)) }, label = { Text( stringResource(id = R.string.prompt_password), @@ -168,9 +173,12 @@ private fun InternetArchiveLoginContent( } ) - AnimatedVisibility(visible = state.isLoginError) { + AnimatedVisibility( + modifier = Modifier.padding(top = 20.dp), + visible = state.isLoginError, + enter = fadeIn(), exit = fadeOut() + ) { Text( - modifier = Modifier.padding(top = 20.dp), text = stringResource(id = R.string.error_incorrect_username_or_password), color = MaterialTheme.colors.error ) @@ -190,16 +198,21 @@ private fun InternetArchiveLoginContent( Text(stringResource(id = R.string.action_cancel)) } Button( + enabled = !state.isBusy, onClick = { dispatch(Login) }, colors = ButtonDefaults.buttonColors( backgroundColor = colorResource(id = R.color.colorPrimary), contentColor = colorResource(id = R.color.colorBackground) ) ) { - Text( - text = stringResource(id = R.string.title_activity_login), - fontSize = 18.sp, - ) + if (state.isBusy) { + CircularProgressIndicator() + } else { + Text( + text = stringResource(id = R.string.title_activity_login), + fontSize = 18.sp, + ) + } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index e1119507..08db583b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,26 +1,10 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import android.os.Bundle -import net.opendasharchive.openarchive.db.Space - data class InternetArchiveLoginState( val email: String = "", val password: String = "", val isEmailError: Boolean = false, val isPasswordError: Boolean = false, val isLoginError: Boolean = false, + val isBusy: Boolean = false ) - -const val ARG_VAL_NEW_SPACE = -1L -const val ARG_SPACE = "space" -fun Bundle?.getSpace(type: Space.Type): Pair { - val mSpaceId = this?.getLong(ARG_SPACE, ARG_VAL_NEW_SPACE) ?: ARG_VAL_NEW_SPACE - - val isNewSpace = ARG_VAL_NEW_SPACE == mSpaceId - - return if (isNewSpace) { - Pair(Space(type), true) - } else { - Space.get(mSpaceId)?.let { Pair(it, false) } ?: Pair(Space(type), true) - } -} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index bbe08a47..7e2e530c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -1,13 +1,13 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive -import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Cancel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.ErrorFade import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess @@ -15,7 +15,7 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.log import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword class InternetArchiveLoginViewModel( - private val repository: InternetArchiveRepository, + private val loginUseCase: InternetArchiveLoginUseCase, private val space: Space, ) : StatefulViewModel(InternetArchiveLoginState()) { @@ -25,25 +25,24 @@ class InternetArchiveLoginViewModel( ): InternetArchiveLoginState = when (action) { is UpdateEmail -> state.copy(email = action.value) is UpdatePassword -> state.copy(password = action.value) - is LoginError -> state.copy(isLoginError = true) - is Action.ErrorFade -> state.copy(isLoginError = false) + is Login -> state.copy(isBusy = true) + is LoginError -> state.copy(isLoginError = true, isBusy = false) + is LoginSuccess, is Cancel -> state.copy(isBusy = false) + is ErrorFade -> state.copy(isLoginError = false) else -> state } override suspend fun effects(state: InternetArchiveLoginState, action: Action) { when (action) { - is Login -> withContext(Dispatchers.IO) { - repository.login(state.email, state.password) - .onSuccess { - space.username = it.auth.access - space.password = it.auth.secret - space.save() - send(LoginSuccess(it)) - }.onFailure { - dispatch(LoginError(it)) + is Login -> + loginUseCase(state.email, state.password) + .onSuccess { ia -> + space.saveAndSetCurrent() + send(LoginSuccess(ia)) } - } - is CreateLogin, is Action.Cancel -> send(action) + .onFailure { dispatch(LoginError(it)) } + + is CreateLogin, is Cancel -> send(action) else -> Unit } } @@ -66,4 +65,9 @@ class InternetArchiveLoginViewModel( data class UpdateEmail(val value: String) : Action data class UpdatePassword(val value: String) : Action } + + private fun Space.saveAndSetCurrent() { + save() + Space.current = this + } } diff --git a/gradle.properties b/gradle.properties index 82e420b5..f9916a37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true -#android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=true android.nonFinalResIds=true From 119dbe93060f6a13df73a05076b4b202f3dd2e4f Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 10:18:22 -0800 Subject: [PATCH 09/27] fix(ia): add missing dependency definition for login use case --- app/build.gradle | 4 ++-- .../openarchive/features/internetarchive/Module.kt | 2 ++ .../openarchive/features/settings/SettingsFragment.kt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a180a0ac..b27a7b6b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { applicationId "net.opendasharchive.openarchive" minSdkVersion 21 targetSdkVersion 34 - versionCode 20549 - versionName '0.3.1-alpha2' + versionCode 20552 + versionName '0.3.2' archivesBaseName = "Save-$versionName" multiDexEnabled true vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 1eea9aaa..fc19e4ea 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -2,6 +2,7 @@ package net.opendasharchive.openarchive.features.internetarchive import com.google.gson.FieldNamingPolicy import com.google.gson.Gson +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository @@ -18,5 +19,6 @@ val internetArchiveModule = module { factory { InternetArchiveRemoteSource(get(), get()) } factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get()) } + factory { InternetArchiveLoginUseCase(get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt index 02ebd64d..8c9acfe2 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsFragment.kt @@ -12,7 +12,7 @@ import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.core.BaseActivity import net.opendasharchive.openarchive.services.dropbox.DropboxActivity import net.opendasharchive.openarchive.services.gdrive.GDriveActivity -import net.opendasharchive.openarchive.services.internetarchive.InternetArchiveActivity +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveActivity import net.opendasharchive.openarchive.services.webdav.WebDavActivity import net.opendasharchive.openarchive.util.extensions.Position import net.opendasharchive.openarchive.util.extensions.getVersionName From bf021c77fd7a11baf73a45c383a6a91fc2ec8116 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 10:21:19 -0800 Subject: [PATCH 10/27] fix(ia): add missing dependency for settings screen --- .../openarchive/features/internetarchive/Module.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index fc19e4ea..7811567f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -6,6 +6,7 @@ import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.I import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository +import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -20,5 +21,6 @@ val internetArchiveModule = module { factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get()) } factory { InternetArchiveLoginUseCase(get()) } + viewModel { args -> InternetArchiveViewModel(args.get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } From 2d1f0e06a76fe5bc9e14526fd817de38a66e5adf Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 11:01:39 -0800 Subject: [PATCH 11/27] fix(ia): add local source for demo fix data model --- .../features/internetarchive/Module.kt | 6 ++-- .../domain/model/InternetArchive.kt | 4 +-- .../datasource/InternetArchiveLocalSource.kt | 18 ++++++++++ .../mapping/InternetArchiveMapper.kt | 4 +-- .../model/InternetArchiveLoginResponse.kt | 3 +- .../repository/InternetArchiveRepository.kt | 7 +++- .../presentation/InternetArchiveActivity.kt | 2 +- .../presentation/InternetArchiveScreen.kt | 22 +++++++----- .../presentation/InternetArchiveState.kt | 4 +-- .../presentation/InternetArchiveViewModel.kt | 34 +++++++++++++++++-- .../login/InternetArchiveLoginScreen.kt | 2 +- 11 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 7811567f..6e6db19a 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -3,6 +3,7 @@ package net.opendasharchive.openarchive.features.internetarchive import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository @@ -18,9 +19,10 @@ val internetArchiveModule = module { .create() } factory { InternetArchiveRemoteSource(get(), get()) } + single { InternetArchiveLocalSource() } factory { InternetArchiveMapper() } - factory { InternetArchiveRepository(get(), get()) } + factory { InternetArchiveRepository(get(), get(), get()) } factory { InternetArchiveLoginUseCase(get()) } - viewModel { args -> InternetArchiveViewModel(args.get()) } + viewModel { args -> InternetArchiveViewModel(get(), args.get()) } viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt index c6c2130e..e809cf88 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt @@ -1,8 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.model data class InternetArchive( - val username: String, + val userName: String, + val screenName: String, val email: String, - val expires: String, val auth: InternetArchiveAuth ) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt new file mode 100644 index 00000000..f1a0cde4 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt @@ -0,0 +1,18 @@ +package net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.update +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive + +class InternetArchiveLocalSource { + // TODO: just use a memory cache for demo, will need to store in DB + private val cache = MutableStateFlow(null) + + fun set(value: InternetArchive) = cache.update { value } + + fun get() = cache.value + + fun subscribe() = cache.filterNotNull() +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index 95541e18..b045de8d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -11,9 +11,9 @@ class InternetArchiveMapper { ) operator fun invoke(response: InternetArchiveLoginResponse.Values) = InternetArchive( - username = response.screenname ?: response.itemname ?: "", + userName = response.itemname ?: "", email = response.email ?: "", - expires = response.expires ?: "", + screenName = response.screenname ?: "", auth = response.s3?.let { invoke(it) } ?: InternetArchiveAuth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt index b81167eb..28734530 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/model/InternetArchiveLoginResponse.kt @@ -6,12 +6,11 @@ data class InternetArchiveLoginResponse( val version: Int, ) { data class Values( - val expires: String? = null, val s3: S3? = null, val screenname: String? = null, val email: String? = null, val itemname: String? = null, - val reason: String? = null + val reason: String? = null, ) data class S3( diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 41435d32..2ea52d8b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -1,9 +1,11 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest @@ -11,6 +13,7 @@ import net.opendasharchive.openarchive.features.internetarchive.infrastructure.m class InternetArchiveRepository( private val remoteSource: InternetArchiveRemoteSource, + private val localSource: InternetArchiveLocalSource, private val mapper: InternetArchiveMapper ) { suspend fun login(email: String, password: String): Result = @@ -24,9 +27,11 @@ class InternetArchiveRepository( when(response.version) { else -> mapper(response.values) } - } + }.onSuccess { localSource.set(it) } } + fun subscribe() = localSource.subscribe() + suspend fun testConnection(auth: InternetArchiveAuth): Result = withContext(Dispatchers.IO) { remoteSource.testConnection(auth).mapCatching { if (!it) throw UnauthenticatedException() } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt index ff9137ec..68ebe874 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -37,7 +37,7 @@ class InternetArchiveActivity : AppCompatActivity() { startActivity(Intent(this, MainActivity::class.java)) measureNewBackend(Space.Type.INTERNET_ARCHIVE) } - IAResult.Cancelled -> Space.navigate(this) + IAResult.Deleted -> Space.navigate(this) else -> Unit } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 7bab2d5d..2c1d2900 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -46,6 +46,7 @@ fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { when (action) { is Action.Remove -> onResult(IAResult.Deleted) is Action.Cancel -> onResult(IAResult.Cancelled) + else -> Unit } } } @@ -56,7 +57,7 @@ fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { @Composable private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { - var isRemoving: Boolean by remember { mutableStateOf(false) } + var isRemoving by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -70,31 +71,33 @@ private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispat Text( modifier = Modifier.padding(top = 12.dp), - text = stringResource(id = R.string.prompt_email), + text = "User Name", style = MaterialTheme.typography.caption ) Text( - text = state.email, + text = state.userName, fontSize = 18.sp ) + Text( modifier = Modifier.padding(top = 12.dp), - text = "Username", + text = "Screen Name", style = MaterialTheme.typography.caption ) + Text( - text = state.username, + text = state.screenName, fontSize = 18.sp ) Text( modifier = Modifier.padding(top = 12.dp), - text = "Expires", + text = "Email", style = MaterialTheme.typography.caption ) Text( - text = state.expires, + text = state.email, fontSize = 18.sp ) @@ -114,6 +117,7 @@ private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispat if (isRemoving) { RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { + isRemoving = false dispatch(Action.Remove) } } @@ -147,8 +151,8 @@ private fun InternetArchiveScreenPreview() { InternetArchiveContent( state = InternetArchiveState( email = "abc@example.com", - username = "@abc_name", - expires = Date().toString() + userName = "@abc_name", + screenName = "ABC Name" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt index dcffbe1d..7c4369fc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt @@ -1,8 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation data class InternetArchiveState( - val username: String = "", + val userName: String = "", + val screenName: String = "", val email: String = "", - val expires: String= "", ) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt index 3e3d906b..2fe13f54 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt @@ -1,13 +1,36 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive +import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action -class InternetArchiveViewModel(private val space: Space) : - StatefulViewModel(InternetArchiveState()) { +class InternetArchiveViewModel( + private val repository: InternetArchiveRepository, + private val space: Space +) : StatefulViewModel(InternetArchiveState()) { - override fun reduce(state: InternetArchiveState, action: Action) = state + init { + viewModelScope.launch { + repository.subscribe().collect { + dispatch(Action.Loaded(it)) + } + } + } + + override fun reduce(state: InternetArchiveState, action: Action) = when(action) { + is Action.Loaded -> state.copy( + userName = action.value.userName, + email = action.value.email, + screenName = action.value.screenName + ) + else -> state + } override suspend fun effects(state: InternetArchiveState, action: Action) { when (action) { @@ -15,11 +38,16 @@ class InternetArchiveViewModel(private val space: Space) : space.delete() send(action) } + is Action.Cancel -> send(action) + else -> Unit } } sealed interface Action { + + data class Loaded(val value: InternetArchive) : Action + data object Remove : Action data object Cancel : Action diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index f9ffbea9..e5f04682 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -206,7 +206,7 @@ private fun InternetArchiveLoginContent( ) ) { if (state.isBusy) { - CircularProgressIndicator() + CircularProgressIndicator(color = colorResource(id = R.color.colorBackground)) } else { Text( text = stringResource(id = R.string.title_activity_login), From 8dd07adab3be68eecde52ca2286e99531ee5f661 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Wed, 6 Mar 2024 11:21:27 -0800 Subject: [PATCH 12/27] fix(ia): spinner color --- .../presentation/login/InternetArchiveLoginScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index e5f04682..e8118254 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -206,7 +206,7 @@ private fun InternetArchiveLoginContent( ) ) { if (state.isBusy) { - CircularProgressIndicator(color = colorResource(id = R.color.colorBackground)) + CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) } else { Text( text = stringResource(id = R.string.title_activity_login), From a5b228f305f9c1982e8e6b8b00dd4759157e2cf0 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Thu, 7 Mar 2024 14:51:05 -0800 Subject: [PATCH 13/27] fix(ia): implement compose theming fix bug in IA login use case that did not set credentials update compose material theme to v3 bump db version and add meta data column for provider extra info --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 2 +- app/src/main/assets/sugar_upgrades/35.sql | 1 + .../openarchive/core/di/FeaturesModule.kt | 3 +- .../infrastructure/client/ClientResult.kt | 3 +- .../core/presentation/theme/Colors.kt | 156 +++++++++++++++ .../core/presentation/theme/Dimensions.kt | 42 ++++ .../core/presentation/theme/Theme.kt | 29 +++ .../opendasharchive/openarchive/db/Space.kt | 1 + .../features/internetarchive/Module.kt | 8 +- .../domain/model/InternetArchive.kt | 20 +- .../domain/model/InternetArchiveAuth.kt | 6 - .../usecase/InternetArchiveLoginUseCase.kt | 21 +- .../datasource/InternetArchiveLocalSource.kt | 3 + .../datasource/InternetArchiveRemoteSource.kt | 4 +- .../mapping/InternetArchiveMapper.kt | 13 +- .../repository/InternetArchiveRepository.kt | 16 +- .../presentation/InternetArchiveActivity.kt | 13 +- .../presentation/InternetArchiveFragment.kt | 11 +- .../presentation/InternetArchiveScreen.kt | 165 ++-------------- .../components/InternetArchiveHeader.kt | 11 +- .../details/InternetArchiveDetailsScreen.kt | 185 ++++++++++++++++++ .../InternetArchiveDetailsState.kt} | 7 +- .../InternetArchiveDetailsViewModel.kt} | 32 +-- .../login/InternetArchiveLoginScreen.kt | 147 +++++++------- .../login/InternetArchiveLoginState.kt | 4 + .../login/InternetArchiveLoginViewModel.kt | 17 +- 27 files changed, 609 insertions(+), 315 deletions(-) create mode 100644 app/src/main/assets/sugar_upgrades/35.sql create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt delete mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt rename app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/{InternetArchiveState.kt => details/InternetArchiveDetailsState.kt} (57%) rename app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/{InternetArchiveViewModel.kt => details/InternetArchiveDetailsViewModel.kt} (59%) diff --git a/app/build.gradle b/app/build.gradle index b27a7b6b..09ec836f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,7 +95,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "androidx.compose.ui:ui:1.6.2" - implementation "androidx.compose.material:material:1.6.2" + implementation "androidx.compose.material3:material3:1.2.1" implementation 'androidx.compose.foundation:foundation:1.6.2' implementation "androidx.compose.ui:ui-tooling-preview:1.6.2" implementation "androidx.activity:activity-compose:1.8.2" @@ -114,7 +114,7 @@ dependencies { // adding web dav support: https://github.com/thegrizzlylabs/sardine-android' implementation "com.github.guardianproject:sardine-android:89f7eae512" - + implementation "com.google.android.material:material:1.11.0" implementation "com.github.bumptech.glide:glide:4.16.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 56ab137e..cdb18c46 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -228,7 +228,7 @@ + android:value="36" /> OkHttpClient.enqueueResult( + +suspend fun OkHttpClient.enqueueResult( request: Request, onResume: (Response) -> T ) = suspendCancellableCoroutine { continuation -> diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt new file mode 100644 index 00000000..428cfb0c --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt @@ -0,0 +1,156 @@ +package net.opendasharchive.openarchive.core.presentation.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +private val c23_nav_drawer_night = Color(0xff101010) +private val c23_darker_grey = Color(0xff212021) +private val c23_dark_grey = Color(0xff333333) +private val c23_medium_grey = Color(0xff696666) +private val c23_grey = Color(0xff9f9f9f) +private val c23_light_grey = Color(0xffe3e3e4) +private val c23_teal_100 = Color(0xff00ffeb) // h=175,3 s=100 v=100 --> +private val c23_teal_90 = Color(0xff00e7d5) // v=90.6 --> +private val c23_teal_80 = Color(0xff00cebe) // v=80.6 --> +private val c23_teal = Color(0xff00b4a6) // v=70.6 --> +private val c23_teal_60 = Color(0xff009b8f) // v=60.6 --> +private val c23_teal_50 = Color(0xff008177) // v=50.6 --> +private val c23_teal_40 = Color(0xff00685f) // v=40.6 --> +private val c23_teal_30 = Color(0xff004e48) // v=30.6 --> +private val c23_teal_20 = Color(0xff003530) // v=20.6 --> +private val c23_teal_10 = Color(0xff001b19) // v=10.6 --> +private val c23_powder_blue = Color(0xffaae6e1) + +@Immutable +data class ColorTheme( + val material: ColorScheme, + val primaryDark: Color = c23_teal_40, + val primaryBright: Color = c23_powder_blue, + + val colorBottomNavbar: Color = material.primary, + + val colorOnBottomNavbar: Color = material.onBackground, + + val colorAddButton: Color = material.background, + val colorOnAddButton: Color = material.onBackground, + val colorNavigationDrawerBackground: Color = material.background, + val colorOnboarding23GetStarted: Color = material.onBackground, + val colorSpaceSetupProgressOn: Color = Color.Black, + val colorSpaceSetupProgressOff: Color = c23_grey, + val colorBackgroundSpaceIcon: Color = c23_light_grey, + + + val colorPill: Color = Color(0xFFE3E3E4), + val colorMediaOverlayIcon: Color = Color.White, + val colorDanger: Color = material.error, + val colorDivider: Color = Color.LightGray, + val colorImageBackground: Color = Color.Black, + val colorFloatIconBackground: Color = Color.Transparent, + val colorSectionHeaderText: Color = Color.Gray, + val colorMediaTitleText: Color = Color.LightGray, + val colorWaveformIndicator: Color = Color(0xffaa0000), + val colorWaveform: Color = Color(0xFF999999) + +) + +private val LightColorScheme = ColorTheme( + material = lightColorScheme( + + primary = c23_teal, + onPrimary = Color.Black, + primaryContainer = c23_teal_90, + onPrimaryContainer = Color.Black, + + secondary = c23_teal, + onSecondary = Color.Black, + secondaryContainer = c23_teal_90, + onSecondaryContainer = Color.Black, + + tertiary = c23_powder_blue, + onTertiary = Color.Black, + tertiaryContainer = c23_powder_blue, + onTertiaryContainer = Color.Black, + + error = Color.Red, + onError = Color.White, + errorContainer = Color.Red, + onErrorContainer = Color.White, + + background = Color.Black, + onBackground = Color.White, + + surface = c23_light_grey, + onSurface = Color.Black, + surfaceVariant = c23_grey, + onSurfaceVariant = Color.Black, + + outline = Color.Black, + inverseOnSurface = Color.White, + inverseSurface = Color.Black, + inversePrimary = Color.Black, + surfaceTint = c23_teal + ), +) + +private val DarkColorScheme = ColorTheme( + material = darkColorScheme( + primary = c23_teal, + onPrimary = Color.White, + primaryContainer = c23_teal_20, + onPrimaryContainer = Color.White, + + secondary = c23_teal, + onSecondary = Color.White, + secondaryContainer = c23_teal_20, + onSecondaryContainer = Color.White, + + tertiary = c23_powder_blue, + onTertiary = Color.Black, + tertiaryContainer = c23_powder_blue, + onTertiaryContainer = Color.Black, + + error = Color.Red, + onError = Color.White, + errorContainer = Color.Red, + onErrorContainer = Color.White, + + background = Color.Black, + onBackground = Color.White, + + surface = c23_darker_grey, + onSurface = Color.White, + surfaceVariant = c23_dark_grey, + onSurfaceVariant = Color.White, + + outline = Color.White, + inverseOnSurface = Color.Black, + inverseSurface = Color.White, + inversePrimary = Color.White, + surfaceTint = c23_teal + ), +) + +fun getThemeColors(isDarkTheme: Boolean) = if (isDarkTheme) DarkColorScheme else LightColorScheme + +val LocalColors = staticCompositionLocalOf { LightColorScheme } + +@Composable +fun textFieldColors() = TextFieldDefaults.colors( + focusedIndicatorColor = ThemeColors.material.primary, + unfocusedIndicatorColor = ThemeColors.material.primary, + focusedLabelColor = ThemeColors.material.primary, + unfocusedLabelColor = ThemeColors.material.primary, + cursorColor = ThemeColors.material.primary, + focusedContainerColor = ThemeColors.material.surface, + unfocusedContainerColor = ThemeColors.material.surface, + disabledContainerColor = ThemeColors.material.surface, + unfocusedTextColor = ThemeColors.material.onSurface, + focusedTextColor = ThemeColors.material.onSurface, + disabledTextColor = ThemeColors.material.onSurface +) \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt new file mode 100644 index 00000000..8c920a80 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt @@ -0,0 +1,42 @@ +package net.opendasharchive.openarchive.core.presentation.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Immutable +data class Elevations(val card: Dp = 12.dp) + +@Immutable +data class Icons( + val small: Dp = 24.dp, + val medium: Dp = 48.dp, + val large: Dp = 72.dp +) + +@Immutable +data class Padding( + val small: Dp = 8.dp, + val medium: Dp = 16.dp, + val large: Dp = 32.dp, +) + +@Immutable +data class DimensionsTheme( + val touchable: Dp = 48.dp, + val padding: Padding = Padding(), + val elevations: Elevations = Elevations(), + val icons: Icons = Icons(), + val bubbleArrow: Dp = 24.dp, + val buttonCorner: Dp = 4.dp +) + +private val DimensionsLight = DimensionsTheme() + +private val DimensionsDark = DimensionsTheme(elevations = Elevations(card = 0.dp)) + +fun getThemeDimensions(isDarkTheme: Boolean) = + if (isDarkTheme) DimensionsDark else DimensionsLight + +val LocalDimensions = staticCompositionLocalOf { DimensionsLight } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt new file mode 100644 index 00000000..60161a12 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt @@ -0,0 +1,29 @@ +package net.opendasharchive.openarchive.core.presentation.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider + +@Composable +fun Theme(content: @Composable () -> Unit) { + val isDarkTheme = isSystemInDarkTheme() + + val colors = getThemeColors(isDarkTheme) + + val dimensions = getThemeDimensions(isDarkTheme) + + CompositionLocalProvider( + LocalDimensions provides dimensions, + LocalColors provides colors, + ) { + MaterialTheme( + colorScheme = colors.material, + content = content + ) + } +} + + +val ThemeColors: ColorTheme @Composable get() = LocalColors.current +val ThemeDimensions: DimensionsTheme @Composable get() = LocalDimensions.current \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt b/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt index e8f9e5dc..56d24470 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/Space.kt @@ -28,6 +28,7 @@ data class Space( var displayname: String = "", var password: String = "", var host: String = "", + var metaData: String = "", private var licenseUrl: String? = null, private var chunking: Boolean? = null ) : SugarRecord() { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index 6e6db19a..b197c213 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -7,7 +7,7 @@ import net.opendasharchive.openarchive.features.internetarchive.infrastructure.d import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel +import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -22,7 +22,7 @@ val internetArchiveModule = module { single { InternetArchiveLocalSource() } factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get(), get()) } - factory { InternetArchiveLoginUseCase(get()) } - viewModel { args -> InternetArchiveViewModel(get(), args.get()) } - viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } + factory { args -> InternetArchiveLoginUseCase(get(), get(), args.get()) } + viewModel { args -> InternetArchiveDetailsViewModel(get(), args.get()) } + viewModel { args -> InternetArchiveLoginViewModel(args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt index e809cf88..843cefd6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchive.kt @@ -1,8 +1,18 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.model data class InternetArchive( - val userName: String, - val screenName: String, - val email: String, - val auth: InternetArchiveAuth -) + val meta: MetaData, + val auth: Auth +) { + data class MetaData( + val userName: String, + val screenName: String, + val email: String, + ) + + + data class Auth( + val access: String, + val secret: String, + ) +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt deleted file mode 100644 index c49806df..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/model/InternetArchiveAuth.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.opendasharchive.openarchive.features.internetarchive.domain.model - -data class InternetArchiveAuth( - val access: String, - val secret: String, -) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt index e2c9bc1c..84ea461a 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/InternetArchiveLoginUseCase.kt @@ -1,15 +1,32 @@ package net.opendasharchive.openarchive.features.internetarchive.domain.usecase +import com.google.gson.Gson +import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository class InternetArchiveLoginUseCase( - private val repository: InternetArchiveRepository + private val repository: InternetArchiveRepository, + private val gson: Gson, + private val space: Space, ) { suspend operator fun invoke(email: String, password: String): Result = repository.login(email, password).mapCatching { response -> - repository.testConnection(response.auth).getOrThrow() + + response.auth.let { auth -> + repository.testConnection(auth).getOrThrow() + space.username = auth.access + space.password = auth.secret + } + + // TODO: use local data source for database + space.metaData = gson.toJson(response.meta) + space.save() + + Space.current = space + response } + } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt index f1a0cde4..01037892 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveLocalSource.kt @@ -8,6 +8,9 @@ import net.opendasharchive.openarchive.features.internetarchive.domain.model.Int class InternetArchiveLocalSource { // TODO: just use a memory cache for demo, will need to store in DB + // the database should be SQLCipher (https://www.zetetic.net/sqlcipher/) + // as we are storing access keys. Sugar record does not support sql cipher + // so planning a migration using local data sources. private val cache = MutableStateFlow(null) fun set(value: InternetArchive) = cache.update { value } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt index 47f0bf26..c8d1d74f 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/datasource/InternetArchiveRemoteSource.kt @@ -3,7 +3,7 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure. import android.content.Context import com.google.gson.Gson import net.opendasharchive.openarchive.core.infrastructure.client.enqueueResult -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth +import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginRequest import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse import net.opendasharchive.openarchive.services.SaveClient @@ -34,7 +34,7 @@ class InternetArchiveRemoteSource( Result.success(data) } - suspend fun testConnection(auth: InternetArchiveAuth): Result = + suspend fun testConnection(auth: InternetArchive.Auth): Result = SaveClient.get(context).enqueueResult( Request.Builder() .url(ARCHIVE_API_ENDPOINT) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt index b045de8d..665403c7 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/mapping/InternetArchiveMapper.kt @@ -1,19 +1,20 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.model.InternetArchiveLoginResponse class InternetArchiveMapper { - private operator fun invoke(response: InternetArchiveLoginResponse.S3) = InternetArchiveAuth( + private operator fun invoke(response: InternetArchiveLoginResponse.S3) = InternetArchive.Auth( access = response.access, secret = response.secret ) operator fun invoke(response: InternetArchiveLoginResponse.Values) = InternetArchive( - userName = response.itemname ?: "", - email = response.email ?: "", - screenName = response.screenname ?: "", - auth = response.s3?.let { invoke(it) } ?: InternetArchiveAuth("", "") + meta = InternetArchive.MetaData( + userName = response.itemname ?: "", + email = response.email ?: "", + screenName = response.screenname ?: "" + ), + auth = response.s3?.let { invoke(it) } ?: InternetArchive.Auth("", "") ) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt index 2ea52d8b..cec01ebc 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/infrastructure/repository/InternetArchiveRepository.kt @@ -1,10 +1,8 @@ package net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive -import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchiveAuth import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper @@ -24,15 +22,15 @@ class InternetArchiveRepository( if (response.success.not()) { throw IllegalArgumentException(response.values.reason) } - when(response.version) { - else -> mapper(response.values) + when (response.version) { + else -> mapper(response.values) } }.onSuccess { localSource.set(it) } } - fun subscribe() = localSource.subscribe() - - suspend fun testConnection(auth: InternetArchiveAuth): Result = withContext(Dispatchers.IO) { - remoteSource.testConnection(auth).mapCatching { if (!it) throw UnauthenticatedException() } - } + suspend fun testConnection(auth: InternetArchive.Auth): Result = + withContext(Dispatchers.IO) { + remoteSource.testConnection(auth) + .mapCatching { if (!it) throw UnauthenticatedException() } + } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt index 68ebe874..3f3cee65 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveActivity.kt @@ -9,24 +9,18 @@ import net.opendasharchive.openarchive.CleanInsightsManager import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen import net.opendasharchive.openarchive.features.main.MainActivity @Deprecated("use jetpack compose") class InternetArchiveActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val (space, isNewSpace) = intent.extras.getSpace(Space.Type.INTERNET_ARCHIVE) setContent { - if (isNewSpace) { - InternetArchiveLoginScreen(space) { - finish(it) - } - } else { - InternetArchiveScreen(space) { - finish(it) - } + InternetArchiveScreen(space, isNewSpace) { + finish(it) } } } @@ -37,6 +31,7 @@ class InternetArchiveActivity : AppCompatActivity() { startActivity(Intent(this, MainActivity::class.java)) measureNewBackend(Space.Type.INTERNET_ARCHIVE) } + IAResult.Deleted -> Space.navigate(this) else -> Unit } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt index 2c7a11ba..45313cae 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveFragment.kt @@ -13,7 +13,6 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.com import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithNewSpace import net.opendasharchive.openarchive.features.internetarchive.presentation.components.bundleWithSpaceId import net.opendasharchive.openarchive.features.internetarchive.presentation.components.getSpace -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen @Deprecated("only used for backward compatibility") class InternetArchiveFragment : Fragment() { @@ -28,14 +27,8 @@ class InternetArchiveFragment : Fragment() { return ComposeView(requireContext()).apply { setContent { - if (isNewSpace) { - InternetArchiveLoginScreen(space) { result -> - finish(result) - } - } else { - InternetArchiveScreen(space) { result -> - finish(result) - } + InternetArchiveScreen(space, isNewSpace) { result -> + finish(result) } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt index 2c1d2900..36338e8d 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveScreen.kt @@ -1,164 +1,21 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -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.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.core.presentation.theme.Theme import net.opendasharchive.openarchive.db.Space -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult -import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader -import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf -import java.util.Date +import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsScreen +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginScreen @Composable -fun InternetArchiveScreen(space: Space, onResult: (IAResult) -> Unit) { - val viewModel: InternetArchiveViewModel = koinViewModel { - parametersOf(space) - } - - val state by viewModel.state.collectAsState() - - LaunchedEffect(Unit) { - viewModel.effects.collect { action -> - when (action) { - is Action.Remove -> onResult(IAResult.Deleted) - is Action.Cancel -> onResult(IAResult.Cancelled) - else -> Unit - } +fun InternetArchiveScreen(space: Space, isNewSpace: Boolean, onFinish: (IAResult) -> Unit) = Theme { + if (isNewSpace) { + InternetArchiveLoginScreen(space) { + onFinish(it) } - } - - InternetArchiveContent(state, viewModel::dispatch) -} - -@Composable -private fun InternetArchiveContent(state: InternetArchiveState, dispatch: Dispatch) { - - var isRemoving by remember { mutableStateOf(false) } - - Box( - modifier = Modifier - .fillMaxSize() - .padding(24.dp) - ) { - - Column { - - InternetArchiveHeader() - - Text( - modifier = Modifier.padding(top = 12.dp), - text = "User Name", - style = MaterialTheme.typography.caption - ) - Text( - text = state.userName, - fontSize = 18.sp - ) - - Text( - modifier = Modifier.padding(top = 12.dp), - text = "Screen Name", - style = MaterialTheme.typography.caption - ) - - Text( - text = state.screenName, - fontSize = 18.sp - ) - - Text( - modifier = Modifier.padding(top = 12.dp), - text = "Email", - style = MaterialTheme.typography.caption - ) - - Text( - text = state.email, - fontSize = 18.sp - ) - - Button( - modifier = Modifier - .padding(top = 12.dp) - .align(Alignment.CenterHorizontally), - onClick = { - isRemoving = true - }, - colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) - ) { - Text(stringResource(id = R.string.menu_delete)) - } + } else { + InternetArchiveDetailsScreen(space) { + onFinish(it) } } - - if (isRemoving) { - RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { - isRemoving = false - dispatch(Action.Remove) - } - } -} - -@Composable -private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> Unit) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(id = R.string.remove_from_app))}, - text = { Text(stringResource(id = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app))}, - dismissButton = { - OutlinedButton( - onClick = onDismiss - ) { - Text(stringResource(id = R.string.action_cancel)) - } - }, confirmButton = { - Button( - onClick = onRemove, - colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error) - ) { - Text(stringResource(id = R.string.remove)) - } - }) -} - -@Composable -@Preview(showBackground = true) -private fun InternetArchiveScreenPreview() { - InternetArchiveContent( - state = InternetArchiveState( - email = "abc@example.com", - userName = "@abc_name", - screenName = "ABC Name" - ) - ) {} -} - -@Composable -@Preview(showBackground = true) -private fun RemoveInternetArchiveDialogPreview() { - RemoveInternetArchiveDialog(onDismiss = { }) {} -} +} \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt index 2c6ddb67..26b9a510 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt @@ -1,5 +1,6 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.components +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -8,7 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,6 +24,8 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.LocalColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors @Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { @@ -46,10 +49,12 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 Text( text = stringResource(id = R.string.internet_archive), fontSize = titleSize, - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, + color = ThemeColors.material.onSurface ) Text( - text = stringResource(id = R.string.internet_archive_description) + text = stringResource(id = R.string.internet_archive_description), + color = ThemeColors.material.onSurfaceVariant ) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt new file mode 100644 index 00000000..3723e2d3 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt @@ -0,0 +1,185 @@ +package net.opendasharchive.openarchive.features.internetarchive.presentation.details + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions +import net.opendasharchive.openarchive.core.state.Dispatch +import net.opendasharchive.openarchive.db.Space +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult +import net.opendasharchive.openarchive.features.internetarchive.presentation.components.InternetArchiveHeader +import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel.Action +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +@Composable +fun InternetArchiveDetailsScreen(space: Space, onResult: (IAResult) -> Unit) { + val viewModel: InternetArchiveDetailsViewModel = koinViewModel { + parametersOf(space) + } + + val state by viewModel.state.collectAsState() + + LaunchedEffect(Unit) { + viewModel.effects.collect { action -> + when (action) { + is Action.Remove -> onResult(IAResult.Deleted) + is Action.Cancel -> onResult(IAResult.Cancelled) + else -> Unit + } + } + } + + InternetArchiveDetailsContent(state, viewModel::dispatch) +} + +@Composable +private fun InternetArchiveDetailsContent( + state: InternetArchiveDetailsState, + dispatch: Dispatch +) { + + var isRemoving by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + ) { + + Column { + + InternetArchiveHeader() + + Spacer(Modifier.height(ThemeDimensions.padding.large)) + + Text( + text = "User Name", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground + ) + Text( + text = state.userName, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + modifier = Modifier.padding(top = 16.dp), + text = "Screen Name", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = state.screenName, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + modifier = Modifier.padding(top = 16.dp), + text = "Email", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground + ) + + Text( + text = state.email, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground + ) + } + + Button( + modifier = Modifier + .padding(top = 12.dp) + .align(Alignment.BottomCenter), + shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + onClick = { + isRemoving = true + }, + colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) + ) { + Text(stringResource(id = R.string.menu_delete)) + } + } + + if (isRemoving) { + RemoveInternetArchiveDialog(onDismiss = { isRemoving = false }) { + isRemoving = false + dispatch(Action.Remove) + } + } +} + +@Composable +private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> Unit) { + AlertDialog( + onDismissRequest = onDismiss, + containerColor = ThemeColors.material.surface, + titleContentColor = ThemeColors.material.onSurface, + textContentColor = ThemeColors.material.onSurfaceVariant, + title = { + Text(text = stringResource(id = R.string.remove_from_app)) + }, + text = { Text(stringResource(id = R.string.are_you_sure_you_want_to_remove_this_server_from_the_app)) }, + dismissButton = { + OutlinedButton( + onClick = onDismiss, + shape = RoundedCornerShape(ThemeDimensions.buttonCorner) + ) { + Text(stringResource(id = R.string.action_cancel)) + } + }, confirmButton = { + Button( + onClick = onRemove, + shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) + ) { + Text(stringResource(id = R.string.remove)) + } + }) +} + +@Composable +@Preview(showBackground = true) +private fun InternetArchiveScreenPreview() { + InternetArchiveDetailsContent( + state = InternetArchiveDetailsState( + email = "abc@example.com", + userName = "@abc_name", + screenName = "ABC Name" + ) + ) {} +} + +@Composable +@Preview(showBackground = true) +private fun RemoveInternetArchiveDialogPreview() { + RemoveInternetArchiveDialog(onDismiss = { }) {} +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsState.kt similarity index 57% rename from app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt rename to app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsState.kt index 7c4369fc..d81558c9 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsState.kt @@ -1,6 +1,9 @@ -package net.opendasharchive.openarchive.features.internetarchive.presentation +package net.opendasharchive.openarchive.features.internetarchive.presentation.details -data class InternetArchiveState( +import androidx.compose.runtime.Immutable + +@Immutable +data class InternetArchiveDetailsState( val userName: String = "", val screenName: String = "", val email: String = "", diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsViewModel.kt similarity index 59% rename from app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt rename to app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsViewModel.kt index 2fe13f54..88ea71f6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/InternetArchiveViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsViewModel.kt @@ -1,29 +1,24 @@ -package net.opendasharchive.openarchive.features.internetarchive.presentation +package net.opendasharchive.openarchive.features.internetarchive.presentation.details import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel -import kotlinx.coroutines.flow.collect +import com.google.gson.Gson import kotlinx.coroutines.launch import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.infrastructure.repository.InternetArchiveRepository -import net.opendasharchive.openarchive.features.internetarchive.presentation.InternetArchiveViewModel.Action +import net.opendasharchive.openarchive.features.internetarchive.presentation.details.InternetArchiveDetailsViewModel.Action -class InternetArchiveViewModel( - private val repository: InternetArchiveRepository, +class InternetArchiveDetailsViewModel( + private val gson: Gson, private val space: Space -) : StatefulViewModel(InternetArchiveState()) { +) : StatefulViewModel(InternetArchiveDetailsState()) { init { - viewModelScope.launch { - repository.subscribe().collect { - dispatch(Action.Loaded(it)) - } - } + dispatch(Action.Load(space)) } - override fun reduce(state: InternetArchiveState, action: Action) = when(action) { + override fun reduce(state: InternetArchiveDetailsState, action: Action) = when(action) { is Action.Loaded -> state.copy( userName = action.value.userName, email = action.value.email, @@ -32,13 +27,18 @@ class InternetArchiveViewModel( else -> state } - override suspend fun effects(state: InternetArchiveState, action: Action) { + override suspend fun effects(state: InternetArchiveDetailsState, action: Action) { when (action) { is Action.Remove -> { space.delete() send(action) } + is Action.Load -> { + val metaData = gson.fromJson(space.metaData, InternetArchive.MetaData::class.java) + dispatch(Action.Loaded(metaData)) + } + is Action.Cancel -> send(action) else -> Unit } @@ -46,7 +46,9 @@ class InternetArchiveViewModel( sealed interface Action { - data class Loaded(val value: InternetArchive) : Action + data class Load(val value: Space) : Action + + data class Loaded(val value: InternetArchive.MetaData) : Action data object Remove : Action diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index e8118254..c7a7563b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -1,6 +1,7 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login import android.content.Intent +import android.content.res.Configuration import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -16,16 +17,18 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -43,6 +46,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R +import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.textFieldColors import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult @@ -63,18 +68,16 @@ fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit) { val state by viewModel.state.collectAsState() - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = {} - ) + val launcher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult(), + onResult = {}) LaunchedEffect(Unit) { viewModel.effects.collect { action -> when (action) { is CreateLogin -> launcher.launch( Intent( - Intent.ACTION_VIEW, - Uri.parse(CreateLogin.URI) + Intent.ACTION_VIEW, Uri.parse(CreateLogin.URI) ) ) @@ -92,8 +95,7 @@ fun InternetArchiveLoginScreen(space: Space, onResult: (IAResult) -> Unit) { @Composable private fun InternetArchiveLoginContent( - state: InternetArchiveLoginState, - dispatch: Dispatch + state: InternetArchiveLoginState, dispatch: Dispatch ) { LaunchedEffect(state.isLoginError) { @@ -109,17 +111,16 @@ private fun InternetArchiveLoginContent( .padding(24.dp) ) { Column( - Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally + Modifier + .align(Alignment.Center) + .padding(bottom = 20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { InternetArchiveHeader( - modifier = Modifier.padding(bottom = 24.dp), - titleSize = 32.sp + modifier = Modifier.padding(bottom = 24.dp), titleSize = 32.sp ) - TextField( - value = state.email, + TextField(value = state.email, enabled = !state.isBusy, onValueChange = { dispatch(UpdateEmail(it)) }, label = { @@ -135,13 +136,7 @@ private fun InternetArchiveLoginContent( keyboardType = KeyboardType.Email ), isError = state.isEmailError, - colors = colorResource(id = R.color.colorPrimary).let { - TextFieldDefaults.textFieldColors( - focusedIndicatorColor = it, - focusedLabelColor = it, - cursorColor = it - ) - } + colors = textFieldColors() ) Spacer(Modifier.height(12.dp)) @@ -164,82 +159,80 @@ private fun InternetArchiveLoginContent( imeAction = ImeAction.Go ), isError = state.isPasswordError, - colors = colorResource(id = R.color.colorPrimary).let { - TextFieldDefaults.textFieldColors( - focusedIndicatorColor = it, - focusedLabelColor = it, - cursorColor = it - ) - } + colors = textFieldColors(), ) AnimatedVisibility( modifier = Modifier.padding(top = 20.dp), visible = state.isLoginError, - enter = fadeIn(), exit = fadeOut() + enter = fadeIn(), + exit = fadeOut() ) { Text( text = stringResource(id = R.string.error_incorrect_username_or_password), - color = MaterialTheme.colors.error + color = MaterialTheme.colorScheme.error ) } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround - ) { - OutlinedButton( - colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colors.onSurface), - onClick = { dispatch(Action.Cancel) } - ) { - Text(stringResource(id = R.string.action_cancel)) - } - Button( - enabled = !state.isBusy, - onClick = { dispatch(Login) }, - colors = ButtonDefaults.buttonColors( - backgroundColor = colorResource(id = R.color.colorPrimary), - contentColor = colorResource(id = R.color.colorBackground) - ) - ) { - if (state.isBusy) { - CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) - } else { - Text( - text = stringResource(id = R.string.title_activity_login), - fontSize = 18.sp, - ) - } - } - } - Row( modifier = Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically ) { Text( - text = "No account?" + text = "No account?", color = ThemeColors.material.onSurface ) - TextButton( - colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), + TextButton(colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), onClick = { dispatch(CreateLogin) }) { Text(text = "Create Login", fontSize = 16.sp, fontWeight = FontWeight.Black) } } } + + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(top = 20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + OutlinedButton(colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface), + shape = RoundedCornerShape(4.dp), + onClick = { dispatch(Action.Cancel) }) { + Text( + text = stringResource(id = R.string.action_cancel), + color = ThemeColors.material.onSurface + ) + } + Button( + enabled = !state.isBusy, + shape = RoundedCornerShape(4.dp), + onClick = { dispatch(Login) }, + colors = ButtonDefaults.buttonColors( + containerColor = colorResource(id = R.color.colorPrimary), + contentColor = colorResource(id = R.color.colorBackground) + ) + ) { + if (state.isBusy) { + CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) + } else { + Text( + text = stringResource(id = R.string.title_activity_login), + fontSize = 18.sp, + ) + } + } + } + } } @Composable -@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( - email = "user@example.org", - password = "abc123" + email = "user@example.org", password = "abc123" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index 08db583b..db6a6853 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,5 +1,9 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable + +@Immutable data class InternetArchiveLoginState( val email: String = "", val password: String = "", diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index 7e2e530c..e2e94139 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -13,11 +13,19 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.log import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword +import org.koin.compose.koinInject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.parameter.parametersOf +import org.koin.java.KoinJavaComponent.inject class InternetArchiveLoginViewModel( - private val loginUseCase: InternetArchiveLoginUseCase, private val space: Space, -) : StatefulViewModel(InternetArchiveLoginState()) { +) : StatefulViewModel(InternetArchiveLoginState()), KoinComponent { + + private val loginUseCase: InternetArchiveLoginUseCase by inject { + parametersOf(space) + } override fun reduce( state: InternetArchiveLoginState, @@ -37,7 +45,6 @@ class InternetArchiveLoginViewModel( is Login -> loginUseCase(state.email, state.password) .onSuccess { ia -> - space.saveAndSetCurrent() send(LoginSuccess(ia)) } .onFailure { dispatch(LoginError(it)) } @@ -66,8 +73,4 @@ class InternetArchiveLoginViewModel( data class UpdatePassword(val value: String) : Action } - private fun Space.saveAndSetCurrent() { - save() - Space.current = this - } } From c8b85f12e40309d55d088d5029264cc0ab934041 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 11 Mar 2024 16:40:46 -0700 Subject: [PATCH 14/27] fix(ia): apply ui changes --- .../presentation/components/PrimaryButton.kt | 9 + .../core/presentation/theme/Colors.kt | 44 ++-- .../core/presentation/theme/Dimensions.kt | 12 +- .../features/internetarchive/Module.kt | 4 +- .../ValidateLoginCredentialsUseCase.kt | 18 ++ .../components/InternetArchiveHeader.kt | 15 +- .../details/InternetArchiveDetailsScreen.kt | 14 +- .../login/InternetArchiveLoginScreen.kt | 205 ++++++++++-------- .../login/InternetArchiveLoginState.kt | 8 +- .../login/InternetArchiveLoginViewModel.kt | 31 ++- app/src/main/res/values/strings.xml | 15 +- 11 files changed, 227 insertions(+), 148 deletions(-) create mode 100644 app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt create mode 100644 app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt new file mode 100644 index 00000000..c7e2ff25 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/components/PrimaryButton.kt @@ -0,0 +1,9 @@ +package net.opendasharchive.openarchive.core.presentation.components + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Button +import androidx.compose.runtime.Composable + +@Composable +fun PrimaryButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) = + Button(onClick = onClick, content = content) diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt index 428cfb0c..e7bfd37a 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt @@ -14,6 +14,7 @@ private val c23_darker_grey = Color(0xff212021) private val c23_dark_grey = Color(0xff333333) private val c23_medium_grey = Color(0xff696666) private val c23_grey = Color(0xff9f9f9f) +private val c23_grey_50 = Color(0xff777979) private val c23_light_grey = Color(0xffe3e3e4) private val c23_teal_100 = Color(0xff00ffeb) // h=175,3 s=100 v=100 --> private val c23_teal_90 = Color(0xff00e7d5) // v=90.6 --> @@ -33,6 +34,9 @@ data class ColorTheme( val primaryDark: Color = c23_teal_40, val primaryBright: Color = c23_powder_blue, + val disabledContainer: Color = c23_teal_20, + val onDisabledContainer: Color = c23_light_grey, + val colorBottomNavbar: Color = material.primary, val colorOnBottomNavbar: Color = material.onBackground, @@ -64,7 +68,7 @@ private val LightColorScheme = ColorTheme( primary = c23_teal, onPrimary = Color.Black, - primaryContainer = c23_teal_90, + primaryContainer = c23_teal, onPrimaryContainer = Color.Black, secondary = c23_teal, @@ -78,9 +82,9 @@ private val LightColorScheme = ColorTheme( onTertiaryContainer = Color.Black, error = Color.Red, - onError = Color.White, + onError = Color.Black, errorContainer = Color.Red, - onErrorContainer = Color.White, + onErrorContainer = Color.Black, background = Color.Black, onBackground = Color.White, @@ -92,7 +96,7 @@ private val LightColorScheme = ColorTheme( outline = Color.Black, inverseOnSurface = Color.White, - inverseSurface = Color.Black, + inverseSurface = c23_dark_grey, inversePrimary = Color.Black, surfaceTint = c23_teal ), @@ -101,14 +105,14 @@ private val LightColorScheme = ColorTheme( private val DarkColorScheme = ColorTheme( material = darkColorScheme( primary = c23_teal, - onPrimary = Color.White, - primaryContainer = c23_teal_20, - onPrimaryContainer = Color.White, + onPrimary = Color.Black, + primaryContainer = c23_teal, + onPrimaryContainer = Color.Black, secondary = c23_teal, - onSecondary = Color.White, + onSecondary = Color.Black, secondaryContainer = c23_teal_20, - onSecondaryContainer = Color.White, + onSecondaryContainer = Color.Black, tertiary = c23_powder_blue, onTertiary = Color.Black, @@ -116,9 +120,9 @@ private val DarkColorScheme = ColorTheme( onTertiaryContainer = Color.Black, error = Color.Red, - onError = Color.White, + onError = Color.Black, errorContainer = Color.Red, - onErrorContainer = Color.White, + onErrorContainer = Color.Black, background = Color.Black, onBackground = Color.White, @@ -129,8 +133,8 @@ private val DarkColorScheme = ColorTheme( onSurfaceVariant = Color.White, outline = Color.White, + inverseSurface = c23_light_grey, inverseOnSurface = Color.Black, - inverseSurface = Color.White, inversePrimary = Color.White, surfaceTint = c23_teal ), @@ -143,14 +147,14 @@ val LocalColors = staticCompositionLocalOf { LightColorScheme } @Composable fun textFieldColors() = TextFieldDefaults.colors( focusedIndicatorColor = ThemeColors.material.primary, - unfocusedIndicatorColor = ThemeColors.material.primary, focusedLabelColor = ThemeColors.material.primary, - unfocusedLabelColor = ThemeColors.material.primary, - cursorColor = ThemeColors.material.primary, focusedContainerColor = ThemeColors.material.surface, - unfocusedContainerColor = ThemeColors.material.surface, - disabledContainerColor = ThemeColors.material.surface, - unfocusedTextColor = ThemeColors.material.onSurface, focusedTextColor = ThemeColors.material.onSurface, - disabledTextColor = ThemeColors.material.onSurface -) \ No newline at end of file + unfocusedIndicatorColor = ThemeColors.material.onSurfaceVariant, + unfocusedContainerColor = ThemeColors.material.surfaceVariant, + unfocusedTextColor = ThemeColors.material.onSurfaceVariant, + unfocusedLabelColor = ThemeColors.material.primary, + cursorColor = ThemeColors.material.primary, + disabledContainerColor = ThemeColors.disabledContainer, + disabledTextColor = ThemeColors.onDisabledContainer +) diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt index 8c920a80..a813a0b6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Dimensions.kt @@ -16,20 +16,22 @@ data class Icons( ) @Immutable -data class Padding( +data class Spacing( + val xsmall: Dp = 4.dp, val small: Dp = 8.dp, val medium: Dp = 16.dp, - val large: Dp = 32.dp, + val large: Dp = 24.dp, + val xlarge: Dp = 32.dp ) @Immutable data class DimensionsTheme( val touchable: Dp = 48.dp, - val padding: Padding = Padding(), + val spacing: Spacing = Spacing(), val elevations: Elevations = Elevations(), val icons: Icons = Icons(), val bubbleArrow: Dp = 24.dp, - val buttonCorner: Dp = 4.dp + val roundedCorner: Dp = 8.dp ) private val DimensionsLight = DimensionsTheme() @@ -39,4 +41,4 @@ private val DimensionsDark = DimensionsTheme(elevations = Elevations(card = 0.dp fun getThemeDimensions(isDarkTheme: Boolean) = if (isDarkTheme) DimensionsDark else DimensionsLight -val LocalDimensions = staticCompositionLocalOf { DimensionsLight } \ No newline at end of file +val LocalDimensions = staticCompositionLocalOf { DimensionsLight } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt index b197c213..2fd1a89e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/Module.kt @@ -3,6 +3,7 @@ package net.opendasharchive.openarchive.features.internetarchive import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.ValidateLoginCredentialsUseCase import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveLocalSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.datasource.InternetArchiveRemoteSource import net.opendasharchive.openarchive.features.internetarchive.infrastructure.mapping.InternetArchiveMapper @@ -18,11 +19,12 @@ val internetArchiveModule = module { .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create() } + factory { ValidateLoginCredentialsUseCase() } factory { InternetArchiveRemoteSource(get(), get()) } single { InternetArchiveLocalSource() } factory { InternetArchiveMapper() } factory { InternetArchiveRepository(get(), get(), get()) } factory { args -> InternetArchiveLoginUseCase(get(), get(), args.get()) } viewModel { args -> InternetArchiveDetailsViewModel(get(), args.get()) } - viewModel { args -> InternetArchiveLoginViewModel(args.get()) } + viewModel { args -> InternetArchiveLoginViewModel(get(), args.get()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt new file mode 100644 index 00000000..d8054ee8 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/domain/usecase/ValidateLoginCredentialsUseCase.kt @@ -0,0 +1,18 @@ +package net.opendasharchive.openarchive.features.internetarchive.domain.usecase + +class ValidateLoginCredentialsUseCase { + + operator fun invoke(identifier: String, factor: String): Boolean { + return if (identifier.contains('@')) { + validateEmail(identifier) + } else { + validateUsername(identifier) + } && validatePassword(factor) + } + + private fun validateEmail(identifier: String) = identifier.isNotBlank() + + private fun validateUsername(identifier: String) = identifier.isNotBlank() + + private fun validatePassword(factor: String) = factor.isNotBlank() +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt index 26b9a510..221fabec 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt @@ -1,13 +1,14 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.components -import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,20 +25,20 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.opendasharchive.openarchive.R -import net.opendasharchive.openarchive.core.presentation.theme.LocalColors import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions @Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { Box(modifier = Modifier - .size(48.dp) + .size(ThemeDimensions.touchable) .background( - color = colorResource(id = R.color.colorBackgroundSpaceIcon), + color = ThemeColors.material.surface, shape = CircleShape ).clip(CircleShape)) { Image( - modifier = Modifier.matchParentSize().padding(12.dp), + modifier = Modifier.matchParentSize().padding(ThemeDimensions.spacing.small), painter = painterResource(id = R.drawable.ic_internet_archive), contentDescription = stringResource( id = R.string.internet_archive @@ -45,7 +46,7 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 colorFilter = tint(colorResource(id = R.color.colorPrimary)) ) } - Column(modifier = Modifier.padding(start = 8.dp)) { + Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.small)) { Text( text = stringResource(id = R.string.internet_archive), fontSize = titleSize, diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt index 3723e2d3..10e0dd4c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/details/InternetArchiveDetailsScreen.kt @@ -75,10 +75,10 @@ private fun InternetArchiveDetailsContent( InternetArchiveHeader() - Spacer(Modifier.height(ThemeDimensions.padding.large)) + Spacer(Modifier.height(ThemeDimensions.spacing.large)) Text( - text = "User Name", + text = stringResource(id = R.string.label_username), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground ) @@ -90,7 +90,7 @@ private fun InternetArchiveDetailsContent( Text( modifier = Modifier.padding(top = 16.dp), - text = "Screen Name", + text = stringResource(id = R.string.label_screen_name), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground ) @@ -103,7 +103,7 @@ private fun InternetArchiveDetailsContent( Text( modifier = Modifier.padding(top = 16.dp), - text = "Email", + text = stringResource(id = R.string.label_email), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onBackground ) @@ -119,7 +119,7 @@ private fun InternetArchiveDetailsContent( modifier = Modifier .padding(top = 12.dp) .align(Alignment.BottomCenter), - shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { isRemoving = true }, @@ -151,14 +151,14 @@ private fun RemoveInternetArchiveDialog(onDismiss: () -> Unit, onRemove: () -> U dismissButton = { OutlinedButton( onClick = onDismiss, - shape = RoundedCornerShape(ThemeDimensions.buttonCorner) + shape = RoundedCornerShape(ThemeDimensions.roundedCorner) ) { Text(stringResource(id = R.string.action_cancel)) } }, confirmButton = { Button( onClick = onRemove, - shape = RoundedCornerShape(ThemeDimensions.buttonCorner), + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), colors = ButtonDefaults.buttonColors(containerColor = ThemeColors.material.error) ) { Text(stringResource(id = R.string.remove)) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index c7a7563b..2e81d3e6 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -22,13 +21,10 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -37,16 +33,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource -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.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors +import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions import net.opendasharchive.openarchive.core.presentation.theme.textFieldColors import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space @@ -55,8 +49,8 @@ import net.opendasharchive.openarchive.features.internetarchive.presentation.com import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateUsername import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -101,129 +95,156 @@ private fun InternetArchiveLoginContent( LaunchedEffect(state.isLoginError) { while (state.isLoginError) { delay(3000) - dispatch(Action.ErrorFade) + dispatch(Action.ErrorClear) } } - Box( + Column( modifier = Modifier .fillMaxSize() - .padding(24.dp) + .padding(ThemeDimensions.spacing.medium), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - Modifier - .align(Alignment.Center) - .padding(bottom = 20.dp), horizontalAlignment = Alignment.CenterHorizontally - ) { - InternetArchiveHeader( - modifier = Modifier.padding(bottom = 24.dp), titleSize = 32.sp - ) + InternetArchiveHeader( + modifier = Modifier.padding(bottom = ThemeDimensions.spacing.large) + ) - TextField(value = state.email, - enabled = !state.isBusy, - onValueChange = { dispatch(UpdateEmail(it)) }, - label = { - Text( - text = stringResource(id = R.string.prompt_email), - color = colorResource(id = R.color.colorPrimary) - ) - }, - singleLine = true, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - autoCorrect = false, - keyboardType = KeyboardType.Email - ), - isError = state.isEmailError, - colors = textFieldColors() - ) + val colors = textFieldColors() - Spacer(Modifier.height(12.dp)) + OutlinedTextField( + value = state.username, + enabled = !state.isBusy, + onValueChange = { dispatch(UpdateUsername(it)) }, + label = { + Text( + text = stringResource(id = R.string.label_username), + color = ThemeColors.material.onBackground + ) + }, + placeholder = { + Text( + text = stringResource(id = R.string.placeholder_email_or_username), + color = ThemeColors.material.onSurfaceVariant + ) + }, + singleLine = true, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + autoCorrect = false, + keyboardType = KeyboardType.Email + ), + isError = state.isUsernameError, + colors = colors + ) - TextField( - value = state.password, - enabled = !state.isBusy, - onValueChange = { dispatch(UpdatePassword(it)) }, - label = { - Text( - stringResource(id = R.string.prompt_password), - color = colorResource(id = R.color.colorPrimary) - ) - }, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - autoCorrect = false, - imeAction = ImeAction.Go - ), - isError = state.isPasswordError, - colors = textFieldColors(), - ) + Spacer(Modifier.height(ThemeDimensions.spacing.large)) - AnimatedVisibility( - modifier = Modifier.padding(top = 20.dp), - visible = state.isLoginError, - enter = fadeIn(), - exit = fadeOut() - ) { + OutlinedTextField( + value = state.password, + enabled = !state.isBusy, + onValueChange = { dispatch(UpdatePassword(it)) }, + label = { Text( - text = stringResource(id = R.string.error_incorrect_username_or_password), - color = MaterialTheme.colorScheme.error + stringResource(id = R.string.label_password), + color = ThemeColors.material.onBackground + ) + }, + placeholder = { + Text( + stringResource(id = R.string.placeholder_password), + color = ThemeColors.material.onSurfaceVariant ) - } - Row( - modifier = Modifier.padding(top = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { + }, + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + autoCorrect = false, + imeAction = ImeAction.Go + ), + isError = state.isPasswordError, + colors = colors, + ) + + AnimatedVisibility( + visible = state.isLoginError, + enter = fadeIn(), + exit = fadeOut() + ) { + Text( + text = stringResource(id = R.string.error_incorrect_username_or_password), + color = MaterialTheme.colorScheme.error + ) + } + Row( + modifier = Modifier + .padding(top = ThemeDimensions.spacing.medium) + .weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.prompt_no_account), + color = ThemeColors.material.onSurface + ) + TextButton( + colors = ButtonDefaults.textButtonColors( + contentColor = colorResource(id = R.color.colorPrimary) + ), + onClick = { dispatch(CreateLogin) }) { Text( - text = "No account?", color = ThemeColors.material.onSurface + text = stringResource(id = R.string.label_create_login), + style = MaterialTheme.typography.bodyLarge ) - TextButton(colors = ButtonDefaults.textButtonColors(contentColor = colorResource(id = R.color.colorPrimary)), - onClick = { dispatch(CreateLogin) }) { - Text(text = "Create Login", fontSize = 16.sp, fontWeight = FontWeight.Black) - } } } Row( modifier = Modifier .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(top = 20.dp), + .padding(top = ThemeDimensions.spacing.medium), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceAround + horizontalArrangement = Arrangement.SpaceEvenly ) { - OutlinedButton(colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.onSurface), - shape = RoundedCornerShape(4.dp), + TextButton( + modifier = Modifier + .weight(1f) + .padding(ThemeDimensions.spacing.small), + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ), + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Action.Cancel) }) { Text( - text = stringResource(id = R.string.action_cancel), - color = ThemeColors.material.onSurface + text = stringResource(id = R.string.action_cancel) ) } Button( - enabled = !state.isBusy, - shape = RoundedCornerShape(4.dp), + modifier = Modifier + .weight(1f) + .padding(ThemeDimensions.spacing.small), + enabled = !state.isBusy && state.isValid, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Login) }, colors = ButtonDefaults.buttonColors( - containerColor = colorResource(id = R.color.colorPrimary), - contentColor = colorResource(id = R.color.colorBackground) + containerColor = ThemeColors.material.primaryContainer, + contentColor = ThemeColors.material.onPrimaryContainer, + disabledContainerColor = ThemeColors.disabledContainer, + disabledContentColor = ThemeColors.onDisabledContainer ) ) { if (state.isBusy) { CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) } else { Text( - text = stringResource(id = R.string.title_activity_login), - fontSize = 18.sp, + text = stringResource(id = R.string.label_login), + style = MaterialTheme.typography.bodyLarge, ) } } } - } } @@ -232,7 +253,7 @@ private fun InternetArchiveLoginContent( private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( - email = "user@example.org", password = "abc123" + username = "user@example.org", password = "abc123" ) ) {} } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt index db6a6853..28ab03c1 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginState.kt @@ -1,14 +1,14 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable @Immutable data class InternetArchiveLoginState( - val email: String = "", + val username: String = "", val password: String = "", - val isEmailError: Boolean = false, + val isUsernameError: Boolean = false, val isPasswordError: Boolean = false, val isLoginError: Boolean = false, - val isBusy: Boolean = false + val isBusy: Boolean = false, + val isValid: Boolean = false, ) diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt index e2e94139..9d212c58 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginViewModel.kt @@ -4,24 +4,25 @@ import net.opendasharchive.openarchive.core.presentation.StatefulViewModel import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.domain.model.InternetArchive import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.InternetArchiveLoginUseCase +import net.opendasharchive.openarchive.features.internetarchive.domain.usecase.ValidateLoginCredentialsUseCase import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Cancel import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.CreateLogin -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.ErrorFade +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.ErrorClear import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.Login import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginError import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.LoginSuccess -import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateEmail import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdatePassword -import org.koin.compose.koinInject +import net.opendasharchive.openarchive.features.internetarchive.presentation.login.InternetArchiveLoginViewModel.Action.UpdateUsername import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.parameter.parametersOf -import org.koin.java.KoinJavaComponent.inject class InternetArchiveLoginViewModel( + private val validateLoginCredentials: ValidateLoginCredentialsUseCase, private val space: Space, -) : StatefulViewModel(InternetArchiveLoginState()), KoinComponent { +) : StatefulViewModel(InternetArchiveLoginState()), + KoinComponent { private val loginUseCase: InternetArchiveLoginUseCase by inject { parametersOf(space) @@ -31,19 +32,27 @@ class InternetArchiveLoginViewModel( state: InternetArchiveLoginState, action: Action ): InternetArchiveLoginState = when (action) { - is UpdateEmail -> state.copy(email = action.value) - is UpdatePassword -> state.copy(password = action.value) + is UpdateUsername -> state.copy( + username = action.value, + isValid = validateLoginCredentials(action.value, state.password) + ) + + is UpdatePassword -> state.copy( + password = action.value, + isValid = validateLoginCredentials(state.username, action.value) + ) + is Login -> state.copy(isBusy = true) is LoginError -> state.copy(isLoginError = true, isBusy = false) is LoginSuccess, is Cancel -> state.copy(isBusy = false) - is ErrorFade -> state.copy(isLoginError = false) + is ErrorClear -> state.copy(isLoginError = false) else -> state } override suspend fun effects(state: InternetArchiveLoginState, action: Action) { when (action) { is Login -> - loginUseCase(state.email, state.password) + loginUseCase(state.username, state.password) .onSuccess { ia -> send(LoginSuccess(ia)) } @@ -63,13 +72,13 @@ class InternetArchiveLoginViewModel( data class LoginError(val value: Throwable) : Action - data object ErrorFade : Action + data object ErrorClear : Action data object CreateLogin : Action { const val URI = "https://archive.org/account/signup" } - data class UpdateEmail(val value: String) : Action + data class UpdateUsername(val value: String) : Action data class UpdatePassword(val value: String) : Action } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a46ca1d7..60dca8c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ Internet Archive - Upload and preserve media in a digital library + Upload your media to a public server. Google Drive Google Drive™ Upload to Google Drive @@ -89,6 +89,12 @@ Username Password + + @string/prompt_email + Enter a email or username + @string/prompt_password + Enter a password + Sign in Incorrect username or password This field is required @@ -286,4 +292,11 @@ %1$s cannot work properly if you don\'t allow it to write to your %2$s. Please try the authorization again and make sure to grant all the access permissions listed. new user + + Email + Screen Name + Login + Create Login + No account? + From 212ab9d390f6eaad009e56055c2060515a1b3a64 Mon Sep 17 00:00:00 2001 From: Ryan Jennings Date: Mon, 11 Mar 2024 22:20:39 -0700 Subject: [PATCH 15/27] fix(ia): fix uploads and simplify theme --- .../core/presentation/theme/Colors.kt | 67 +++++------------ .../core/presentation/theme/Theme.kt | 10 ++- .../components/InternetArchiveHeader.kt | 28 ++++--- .../login/InternetArchiveLoginScreen.kt | 74 ++++++------------- .../openarchive/features/main/MainActivity.kt | 60 ++++++++------- .../features/main/MainMediaFragment.kt | 8 +- .../openarchive/services/SaveClient.kt | 6 +- .../services/internetarchive/IaConduit.kt | 16 +--- .../internetarchive/RequestBodyUtil.kt | 28 +++++-- .../openarchive/upload/BroadcastManager.kt | 4 +- .../layout/fragment_space_setup_success.xml | 6 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 4 + 13 files changed, 140 insertions(+), 172 deletions(-) diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt index e7bfd37a..e4e8db82 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Colors.kt @@ -1,10 +1,8 @@ package net.opendasharchive.openarchive.core.presentation.theme import androidx.compose.material3.ColorScheme -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color @@ -36,31 +34,6 @@ data class ColorTheme( val disabledContainer: Color = c23_teal_20, val onDisabledContainer: Color = c23_light_grey, - - val colorBottomNavbar: Color = material.primary, - - val colorOnBottomNavbar: Color = material.onBackground, - - val colorAddButton: Color = material.background, - val colorOnAddButton: Color = material.onBackground, - val colorNavigationDrawerBackground: Color = material.background, - val colorOnboarding23GetStarted: Color = material.onBackground, - val colorSpaceSetupProgressOn: Color = Color.Black, - val colorSpaceSetupProgressOff: Color = c23_grey, - val colorBackgroundSpaceIcon: Color = c23_light_grey, - - - val colorPill: Color = Color(0xFFE3E3E4), - val colorMediaOverlayIcon: Color = Color.White, - val colorDanger: Color = material.error, - val colorDivider: Color = Color.LightGray, - val colorImageBackground: Color = Color.Black, - val colorFloatIconBackground: Color = Color.Transparent, - val colorSectionHeaderText: Color = Color.Gray, - val colorMediaTitleText: Color = Color.LightGray, - val colorWaveformIndicator: Color = Color(0xffaa0000), - val colorWaveform: Color = Color(0xFF999999) - ) private val LightColorScheme = ColorTheme( @@ -86,19 +59,24 @@ private val LightColorScheme = ColorTheme( errorContainer = Color.Red, onErrorContainer = Color.Black, - background = Color.Black, - onBackground = Color.White, + background = Color.White, + onBackground = Color.Black, surface = c23_light_grey, onSurface = Color.Black, surfaceVariant = c23_grey, - onSurfaceVariant = Color.Black, + onSurfaceVariant = c23_darker_grey, outline = Color.Black, inverseOnSurface = Color.White, inverseSurface = c23_dark_grey, inversePrimary = Color.Black, - surfaceTint = c23_teal + surfaceTint = c23_teal, + outlineVariant = c23_darker_grey, + scrim = c23_light_grey, + surfaceBright = c23_light_grey, + surfaceContainer = Color.White, + surfaceDim = c23_light_grey ), ) @@ -107,12 +85,12 @@ private val DarkColorScheme = ColorTheme( primary = c23_teal, onPrimary = Color.Black, primaryContainer = c23_teal, - onPrimaryContainer = Color.Black, + onPrimaryContainer = Color.White, secondary = c23_teal, onSecondary = Color.Black, secondaryContainer = c23_teal_20, - onSecondaryContainer = Color.Black, + onSecondaryContainer = Color.White, tertiary = c23_powder_blue, onTertiary = Color.Black, @@ -130,13 +108,18 @@ private val DarkColorScheme = ColorTheme( surface = c23_darker_grey, onSurface = Color.White, surfaceVariant = c23_dark_grey, - onSurfaceVariant = Color.White, + onSurfaceVariant = c23_light_grey, outline = Color.White, inverseSurface = c23_light_grey, inverseOnSurface = Color.Black, inversePrimary = Color.White, - surfaceTint = c23_teal + surfaceTint = c23_teal, + outlineVariant = c23_light_grey, + scrim = c23_light_grey, + surfaceBright = c23_grey, + surfaceContainer = c23_medium_grey, + surfaceDim = c23_dark_grey ), ) @@ -144,17 +127,3 @@ fun getThemeColors(isDarkTheme: Boolean) = if (isDarkTheme) DarkColorScheme else val LocalColors = staticCompositionLocalOf { LightColorScheme } -@Composable -fun textFieldColors() = TextFieldDefaults.colors( - focusedIndicatorColor = ThemeColors.material.primary, - focusedLabelColor = ThemeColors.material.primary, - focusedContainerColor = ThemeColors.material.surface, - focusedTextColor = ThemeColors.material.onSurface, - unfocusedIndicatorColor = ThemeColors.material.onSurfaceVariant, - unfocusedContainerColor = ThemeColors.material.surfaceVariant, - unfocusedTextColor = ThemeColors.material.onSurfaceVariant, - unfocusedLabelColor = ThemeColors.material.primary, - cursorColor = ThemeColors.material.primary, - disabledContainerColor = ThemeColors.disabledContainer, - disabledTextColor = ThemeColors.onDisabledContainer -) diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt index 60161a12..e0bfbea0 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/presentation/theme/Theme.kt @@ -4,10 +4,14 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState @Composable -fun Theme(content: @Composable () -> Unit) { - val isDarkTheme = isSystemInDarkTheme() +fun Theme( + content: @Composable () -> Unit +) { + val isDarkTheme by rememberUpdatedState(newValue = isSystemInDarkTheme()) val colors = getThemeColors(isDarkTheme) @@ -26,4 +30,4 @@ fun Theme(content: @Composable () -> Unit) { val ThemeColors: ColorTheme @Composable get() = LocalColors.current -val ThemeDimensions: DimensionsTheme @Composable get() = LocalDimensions.current \ No newline at end of file +val ThemeDimensions: DimensionsTheme @Composable get() = LocalDimensions.current diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt index 221fabec..2f6d1a07 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/components/InternetArchiveHeader.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,15 +29,24 @@ import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions @Composable fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 18.sp) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { - Box(modifier = Modifier - .size(ThemeDimensions.touchable) - .background( - color = ThemeColors.material.surface, - shape = CircleShape - ).clip(CircleShape)) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(ThemeDimensions.touchable) + .background( + color = ThemeColors.material.surface, + shape = CircleShape + ) + .clip(CircleShape) + ) { Image( - modifier = Modifier.matchParentSize().padding(ThemeDimensions.spacing.small), + modifier = Modifier + .matchParentSize() + .padding(11.dp), painter = painterResource(id = R.drawable.ic_internet_archive), contentDescription = stringResource( id = R.string.internet_archive @@ -46,7 +54,7 @@ fun InternetArchiveHeader(modifier: Modifier = Modifier, titleSize: TextUnit = 1 colorFilter = tint(colorResource(id = R.color.colorPrimary)) ) } - Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.small)) { + Column(modifier = Modifier.padding(start = ThemeDimensions.spacing.medium)) { Text( text = stringResource(id = R.string.internet_archive), fontSize = titleSize, diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt index 2e81d3e6..28fd81a4 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/internetarchive/presentation/login/InternetArchiveLoginScreen.kt @@ -1,7 +1,6 @@ package net.opendasharchive.openarchive.features.internetarchive.presentation.login import android.content.Intent -import android.content.res.Configuration import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -15,11 +14,11 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -31,8 +30,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource +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.input.PasswordVisualTransformation @@ -41,7 +40,6 @@ import kotlinx.coroutines.delay import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.core.presentation.theme.ThemeColors import net.opendasharchive.openarchive.core.presentation.theme.ThemeDimensions -import net.opendasharchive.openarchive.core.presentation.theme.textFieldColors import net.opendasharchive.openarchive.core.state.Dispatch import net.opendasharchive.openarchive.db.Space import net.opendasharchive.openarchive.features.internetarchive.presentation.components.IAResult @@ -110,23 +108,15 @@ private fun InternetArchiveLoginContent( modifier = Modifier.padding(bottom = ThemeDimensions.spacing.large) ) - val colors = textFieldColors() - OutlinedTextField( value = state.username, enabled = !state.isBusy, onValueChange = { dispatch(UpdateUsername(it)) }, label = { - Text( - text = stringResource(id = R.string.label_username), - color = ThemeColors.material.onBackground - ) + Text(stringResource(R.string.label_username)) }, placeholder = { - Text( - text = stringResource(id = R.string.placeholder_email_or_username), - color = ThemeColors.material.onSurfaceVariant - ) + Text(stringResource(R.string.placeholder_email_or_username)) }, singleLine = true, shape = RoundedCornerShape(ThemeDimensions.roundedCorner), @@ -136,7 +126,6 @@ private fun InternetArchiveLoginContent( keyboardType = KeyboardType.Email ), isError = state.isUsernameError, - colors = colors ) Spacer(Modifier.height(ThemeDimensions.spacing.large)) @@ -146,19 +135,13 @@ private fun InternetArchiveLoginContent( enabled = !state.isBusy, onValueChange = { dispatch(UpdatePassword(it)) }, label = { - Text( - stringResource(id = R.string.label_password), - color = ThemeColors.material.onBackground - ) + Text(stringResource(R.string.label_password)) }, placeholder = { - Text( - stringResource(id = R.string.placeholder_password), - color = ThemeColors.material.onSurfaceVariant - ) - + Text(stringResource(R.string.placeholder_password)) }, singleLine = true, + shape = RoundedCornerShape(ThemeDimensions.roundedCorner), visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Password, @@ -166,7 +149,6 @@ private fun InternetArchiveLoginContent( imeAction = ImeAction.Go ), isError = state.isPasswordError, - colors = colors, ) AnimatedVisibility( @@ -175,27 +157,26 @@ private fun InternetArchiveLoginContent( exit = fadeOut() ) { Text( - text = stringResource(id = R.string.error_incorrect_username_or_password), + text = stringResource(R.string.error_incorrect_username_or_password), color = MaterialTheme.colorScheme.error ) } Row( modifier = Modifier - .padding(top = ThemeDimensions.spacing.medium) + .padding(top = ThemeDimensions.spacing.small) .weight(1f), verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(id = R.string.prompt_no_account), - color = ThemeColors.material.onSurface + text = stringResource(R.string.prompt_no_account), + color = ThemeColors.material.onBackground ) TextButton( - colors = ButtonDefaults.textButtonColors( - contentColor = colorResource(id = R.color.colorPrimary) - ), + modifier = Modifier.heightIn(ThemeDimensions.touchable), onClick = { dispatch(CreateLogin) }) { Text( - text = stringResource(id = R.string.label_create_login), + text = stringResource(R.string.label_create_login), + fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyLarge ) } @@ -211,37 +192,24 @@ private fun InternetArchiveLoginContent( TextButton( modifier = Modifier .weight(1f) + .heightIn(ThemeDimensions.touchable) .padding(ThemeDimensions.spacing.small), - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.primary - ), shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Action.Cancel) }) { - Text( - text = stringResource(id = R.string.action_cancel) - ) + Text(stringResource(R.string.action_cancel)) } Button( modifier = Modifier - .weight(1f) - .padding(ThemeDimensions.spacing.small), + .heightIn(ThemeDimensions.touchable) + .weight(1f), enabled = !state.isBusy && state.isValid, shape = RoundedCornerShape(ThemeDimensions.roundedCorner), onClick = { dispatch(Login) }, - colors = ButtonDefaults.buttonColors( - containerColor = ThemeColors.material.primaryContainer, - contentColor = ThemeColors.material.onPrimaryContainer, - disabledContainerColor = ThemeColors.disabledContainer, - disabledContentColor = ThemeColors.onDisabledContainer - ) ) { if (state.isBusy) { - CircularProgressIndicator(color = colorResource(id = R.color.colorPrimary)) + CircularProgressIndicator(color = ThemeColors.material.primary) } else { - Text( - text = stringResource(id = R.string.label_login), - style = MaterialTheme.typography.bodyLarge, - ) + Text(stringResource(R.string.label_login)) } } } @@ -249,7 +217,7 @@ private fun InternetArchiveLoginContent( } @Composable -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true) private fun InternetArchiveLoginPreview() { InternetArchiveLoginContent( state = InternetArchiveLoginState( diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt index 34e4aefc..e3f27aed 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainActivity.kt @@ -297,15 +297,17 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener private fun refreshSpace() { val currentSpace = Space.current - if (currentSpace != null) { - mBinding.space.setDrawable( - currentSpace.getAvatar(this) - ?.scaled(32, this), Position.Start, tint = false - ) - mBinding.space.text = currentSpace.friendlyName - } else { - mBinding.space.setDrawable(R.drawable.avatar_default, Position.Start, tint = false) - mBinding.space.text = getString(R.string.app_name) + MainScope().launch { + if (currentSpace != null) { + mBinding.space.setDrawable( + currentSpace.getAvatar(this@MainActivity) + ?.scaled(32, this@MainActivity), Position.Start, tint = false + ) + mBinding.space.text = currentSpace.friendlyName + } else { + mBinding.space.setDrawable(R.drawable.avatar_default, Position.Start, tint = false) + mBinding.space.text = getString(R.string.app_name) + } } mSpaceAdapter.update(Space.getAll().asSequence().toList()) @@ -332,17 +334,19 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener private fun refreshCurrentProject() { val project = getSelectedProject() - if (project != null) { - mPagerAdapter.notifyProjectChanged(project) + MainScope().launch { + if (project != null) { + mPagerAdapter.notifyProjectChanged(project) - project.space?.setAvatar(mBinding.currentFolderIcon) - mBinding.currentFolderIcon.show() + project.space?.setAvatar(mBinding.currentFolderIcon) + mBinding.currentFolderIcon.show() - mBinding.currentFolderName.text = project.description - mBinding.currentFolderName.show() - } else { - mBinding.currentFolderIcon.cloak() - mBinding.currentFolderName.cloak() + mBinding.currentFolderName.text = project.description + mBinding.currentFolderName.show() + } else { + mBinding.currentFolderIcon.cloak() + mBinding.currentFolderName.cloak() + } } refreshCurrentFolderCount() @@ -351,16 +355,18 @@ class MainActivity : BaseActivity(), FolderAdapterListener, SpaceAdapterListener private fun refreshCurrentFolderCount() { val project = getSelectedProject() - if (project != null) { - mBinding.currentFolderCount.text = NumberFormat.getInstance().format( - project.collections.map { it.size } - .reduceOrNull { acc, count -> acc + count } ?: 0) - mBinding.currentFolderCount.show() + MainScope().launch { + if (project != null) { + mBinding.currentFolderCount.text = NumberFormat.getInstance().format( + project.collections.map { it.size } + .reduceOrNull { acc, count -> acc + count } ?: 0) + mBinding.currentFolderCount.show() - mBinding.uploadEditButton.toggle(project.isUploading) - } else { - mBinding.currentFolderCount.cloak() - mBinding.uploadEditButton.hide() + mBinding.uploadEditButton.toggle(project.isUploading) + } else { + mBinding.currentFolderCount.cloak() + mBinding.uploadEditButton.hide() + } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt index 51560572..2d6d8480 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt @@ -71,13 +71,13 @@ class MainMediaFragment : Fragment() { setHasOptionsMenu(true) } - override fun onStart() { - super.onStart() + override fun onResume() { + super.onResume() BroadcastManager.register(requireContext(), mMessageReceiver) } - override fun onStop() { - super.onStop() + override fun onPause() { + super.onPause() BroadcastManager.unregister(requireContext(), mMessageReceiver) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt index 42912156..e82a04ce 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt @@ -35,9 +35,9 @@ class SaveClient(context: Context) : StrongBuilderBase okBuilder = OkHttpClient.Builder() .addInterceptor(cacheInterceptor) - .connectTimeout(20L, TimeUnit.SECONDS) - .writeTimeout(20L, TimeUnit.SECONDS) - .readTimeout(20L, TimeUnit.SECONDS) + .connectTimeout(60L, TimeUnit.SECONDS) + .writeTimeout(60L, TimeUnit.SECONDS) + .readTimeout(60L, TimeUnit.SECONDS) .retryOnConnectionFailure(false) .protocols(arrayListOf(Protocol.HTTP_1_1)) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt index 1b3322d6..916a99c0 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt @@ -9,7 +9,6 @@ import net.opendasharchive.openarchive.services.Conduit import net.opendasharchive.openarchive.services.SaveClient import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okio.BufferedSink import java.io.File import java.io.IOException @@ -68,15 +67,7 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { @Throws(IOException::class) private suspend fun uploadMetaData(content: String, basePath: String, fileName: String) { - val requestBody = object : RequestBody() { - override fun contentType(): MediaType? { - return "texts".toMediaTypeOrNull() - } - - override fun writeTo(sink: BufferedSink) { - sink.writeString(content, Charsets.UTF_8) - } - } + val requestBody = RequestBodyUtil.create(content) put( "$ARCHIVE_API_ENDPOINT/$basePath/$fileName.meta.json", @@ -91,7 +82,6 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { val requestBody = getRequestBodyMetaData( uploadFile, Uri.fromFile(uploadFile).toString(), - "texts".toMediaTypeOrNull() ) put("$ARCHIVE_API_ENDPOINT/$basePath/${uploadFile.name}", @@ -127,7 +117,7 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { } /// request body for meta data - private fun getRequestBodyMetaData(media: File, mediaUri: String, mediaType: MediaType?): RequestBody { + private fun getRequestBodyMetaData(media: File, mediaUri: String, mediaType: MediaType? = "texts".toMediaTypeOrNull()): RequestBody { return RequestBodyUtil.create( mContext.contentResolver, Uri.parse(mediaUri), @@ -256,4 +246,4 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { } }) } -} \ No newline at end of file +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt index 352200d2..1635f1e5 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt @@ -1,14 +1,14 @@ package net.opendasharchive.openarchive.services.internetarchive -import okio.source -import okhttp3.internal.closeQuietly -import okhttp3.RequestBody -import kotlin.Throws -import okio.BufferedSink import android.content.ContentResolver import android.net.Uri import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okhttp3.internal.closeQuietly +import okio.BufferedSink import okio.Source +import okio.source import timber.log.Timber import java.io.* @@ -145,4 +145,20 @@ object RequestBodyUtil { } } } -} \ No newline at end of file + + fun create(content: String, mediaType: MediaType? = "texts".toMediaTypeOrNull()): RequestBody { + return object : RequestBody() { + override fun contentType(): MediaType? { + return mediaType + } + + override fun contentLength(): Long { + return content.length.toLong() + } + + override fun writeTo(sink: BufferedSink) { + sink.writeString(content, Charsets.UTF_8) + } + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt index 7d3ec687..08f92adf 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt @@ -30,7 +30,7 @@ object BroadcastManager { } fun getAction(intent: Intent): Action? { - val action = Action.values().firstOrNull { it.id == intent.action } + val action = Action.entries.firstOrNull { it.id == intent.action } action?.mediaId = intent.getLongExtra(MEDIA_ID, -1) return action @@ -47,4 +47,4 @@ object BroadcastManager { fun unregister(context: Context, receiver: BroadcastReceiver) { LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver) } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_space_setup_success.xml b/app/src/main/res/layout/fragment_space_setup_success.xml index b9619941..9413ef00 100644 --- a/app/src/main/res/layout/fragment_space_setup_success.xml +++ b/app/src/main/res/layout/fragment_space_setup_success.xml @@ -49,14 +49,16 @@