Refactor the Authentication Screen

introduction of new theming / navigation / animation.
This commit is contained in:
Thomas Andres Gomez 2022-04-20 23:14:32 +02:00
parent 9074b78046
commit 2a69b75d9d
64 changed files with 1809 additions and 395 deletions

View file

@ -2,7 +2,7 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'org.jetbrains.kotlin.android'
id 'dagger.hilt.android.plugin'
}
android {
@ -20,16 +20,20 @@ android {
vectorDrawables {
useSupportLibrary true
}
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation" : "$projectDir/schemas".toString(),
"room.incremental" : "true",
"room.expandProjection": "true"
]
}
}
}
signingConfigs {
debug {
storeFile file(project.properties["PIXELIZED_DEBUG_STORE_FILE"])
storePassword project.properties["PIXELIZED_DEBUG_STORE_PASSWORD"]
keyAlias project.properties["PIXELIZED_DEBUG_KEY_ALIAS"]
keyPassword project.properties["PIXELIZED_DEBUG_KEY_PASSWORD"]
}
release {
pixelized {
storeFile file(project.properties["PIXELIZED_RELEASE_STORE_FILE"])
storePassword project.properties["PIXELIZED_RELEASE_STORE_PASSWORD"]
keyAlias project.properties["PIXELIZED_RELEASE_KEY_ALIAS"]
@ -39,65 +43,81 @@ android {
buildTypes {
debug {
signingConfig signingConfigs.release
signingConfig signingConfigs.pixelized
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
signingConfig signingConfigs.release
minifyEnabled false
signingConfig signingConfigs.pixelized
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
compose true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.0.0'
kotlinCompilerExtensionVersion '1.1.0'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
// Android core
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
implementation 'androidx.activity:activity-compose:1.4.0'
// Android Compose
implementation "androidx.compose.ui:ui:1.2.0-alpha03"
implementation "androidx.compose.material:material:1.1.1"
implementation "androidx.compose.runtime:runtime-livedata:1.1.1"
implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"
debugImplementation "androidx.compose.ui:ui-tooling:1.1.1"
// Android compose
implementation "androidx.compose.ui:ui:1.0.5"
// Tooling support (Previews, etc.)
implementation "androidx.compose.ui:ui-tooling:1.0.5"
// Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
implementation "androidx.compose.foundation:foundation:1.0.5"
// Material Design
implementation "androidx.compose.material:material:1.0.5"
// Material design icons
implementation "androidx.compose.material:material-icons-core:1.0.5"
implementation "androidx.compose.material:material-icons-extended:1.0.5"
// Integration with activities
implementation "androidx.activity:activity-compose:1.4.0"
// Integration with ViewModels
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0"
// Integration with observables
implementation "androidx.compose.runtime:runtime-livedata:1.0.5"
// ConstraintLayout
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc02"
implementation "androidx.compose.material:material-icons-core:1.1.1"
implementation "androidx.compose.material:material-icons-extended:1.1.1"
// Injection
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
implementation "com.google.dagger:hilt-android:2.40.5"
kapt "com.google.dagger:hilt-compiler:2.40.5"
// Accompanist
implementation "com.google.accompanist:accompanist-systemuicontroller:0.24.3-alpha"
implementation "com.google.accompanist:accompanist-insets:0.24.3-alpha"
// Navigation
implementation "androidx.navigation:navigation-compose:2.4.2"
// Splash Screen support prior to Android 12
implementation "androidx.core:core-splashscreen:1.0.0-beta02"
// Google sign in.
implementation "com.google.android.gms:play-services-auth:19.2.0"
// Paging
implementation "androidx.paging:paging-compose:1.0.0-alpha14"
implementation "com.google.android.gms:play-services-auth:20.1.0"
// RetroFit & Gson for webservice call
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
@ -108,18 +128,16 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:4.8.1"
// Room
implementation "androidx.room:room-runtime:2.3.0"
implementation "androidx.room:room-ktx:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
// Test
testImplementation 'junit:junit:4.13.2'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.0.5"
implementation "androidx.room:room-runtime:2.4.2"
implementation "androidx.room:room-ktx:2.4.2"
kapt "androidx.room:room-compiler:2.4.2"
implementation "androidx.paging:paging-compose:1.0.0-alpha14"
}
static def generateVersionCode() {
def result = "git rev-list HEAD --count".execute().text.trim() //unix
if (result.empty) result = "PowerShell -Command git rev-list HEAD --count".execute().text.trim() //windows
if (result.empty) result = "PowerShell -Command git rev-list HEAD --count".execute().text.trim()
//windows
if (result.empty) throw new RuntimeException("Could not generate versioncode on this platform? Cmd output: ${result.text}")
return result.toInteger()
}

View file

@ -0,0 +1,290 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ee46b997bb81036c29a482590e47e98c",
"entities": [
{
"tableName": "AUTHOR",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`AUTHOR_ID` TEXT NOT NULL, `AUTHOR_NAME` TEXT NOT NULL, `AUTHOR_SORT` TEXT NOT NULL, PRIMARY KEY(`AUTHOR_ID`))",
"fields": [
{
"fieldPath": "id",
"columnName": "AUTHOR_ID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "AUTHOR_NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sort",
"columnName": "AUTHOR_SORT",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"AUTHOR_ID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "BOOK",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`BOOK_ID` INTEGER NOT NULL, `BOOK_TITLE` TEXT NOT NULL, `BOOK_SORT` TEXT NOT NULL, `BOOK_HAVE_COVER` INTEGER NOT NULL, `BOOK_RELEASE_DATE` INTEGER NOT NULL, `BOOK_LANGUAGE_ID` TEXT, `BOOK_RATING` INTEGER, `BOOK_SERIES_ID` TEXT, `BOOK_SYNOPSIS` TEXT, `BOOK_ISNEW` INTEGER NOT NULL, `BOOK_NEW_ORDER` INTEGER, PRIMARY KEY(`BOOK_ID`))",
"fields": [
{
"fieldPath": "id",
"columnName": "BOOK_ID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "BOOK_TITLE",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sort",
"columnName": "BOOK_SORT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "haveCover",
"columnName": "BOOK_HAVE_COVER",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "releaseDate",
"columnName": "BOOK_RELEASE_DATE",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "language",
"columnName": "BOOK_LANGUAGE_ID",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "rating",
"columnName": "BOOK_RATING",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "series",
"columnName": "BOOK_SERIES_ID",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "synopsis",
"columnName": "BOOK_SYNOPSIS",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isNew",
"columnName": "BOOK_ISNEW",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "newOrder",
"columnName": "BOOK_NEW_ORDER",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"BOOK_ID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "GENRE",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GENRE_ID` TEXT NOT NULL, `GENRE_NAME` TEXT NOT NULL, PRIMARY KEY(`GENRE_ID`))",
"fields": [
{
"fieldPath": "id",
"columnName": "GENRE_ID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "GENRE_NAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"GENRE_ID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "LANGUAGE",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LANGUAGE_ID` TEXT NOT NULL, `LANGUAGE_NAME` TEXT NOT NULL, PRIMARY KEY(`LANGUAGE_ID`))",
"fields": [
{
"fieldPath": "id",
"columnName": "LANGUAGE_ID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "code",
"columnName": "LANGUAGE_NAME",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"LANGUAGE_ID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SERIES",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`SERIES_ID` TEXT NOT NULL, `SERIES_NAME` TEXT NOT NULL, `SERIES_SORT` TEXT NOT NULL, `SERIES_INDEX` INTEGER, PRIMARY KEY(`SERIES_ID`))",
"fields": [
{
"fieldPath": "id",
"columnName": "SERIES_ID",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "SERIES_NAME",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sort",
"columnName": "SERIES_SORT",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "SERIES_INDEX",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"SERIES_ID"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "BookAuthorCrossRef",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`BOOK_ID` INTEGER NOT NULL, `AUTHOR_ID` TEXT NOT NULL, PRIMARY KEY(`BOOK_ID`, `AUTHOR_ID`))",
"fields": [
{
"fieldPath": "bookId",
"columnName": "BOOK_ID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorId",
"columnName": "AUTHOR_ID",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"BOOK_ID",
"AUTHOR_ID"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_BookAuthorCrossRef_AUTHOR_ID",
"unique": false,
"columnNames": [
"AUTHOR_ID"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_BookAuthorCrossRef_AUTHOR_ID` ON `${TABLE_NAME}` (`AUTHOR_ID`)"
}
],
"foreignKeys": []
},
{
"tableName": "BookGenreCrossRef",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`BOOK_ID` INTEGER NOT NULL, `GENRE_ID` TEXT NOT NULL, PRIMARY KEY(`BOOK_ID`, `GENRE_ID`))",
"fields": [
{
"fieldPath": "bookId",
"columnName": "BOOK_ID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "genreId",
"columnName": "GENRE_ID",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"BOOK_ID",
"GENRE_ID"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_BookGenreCrossRef_GENRE_ID",
"unique": false,
"columnNames": [
"GENRE_ID"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_BookGenreCrossRef_GENRE_ID` ON `${TABLE_NAME}` (`GENRE_ID`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ee46b997bb81036c29a482590e47e98c')"
]
}
}

View file

@ -1,52 +1,7 @@
package com.pixelized.biblib
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.pixelized.biblib.database.BibLibDatabase
import com.pixelized.biblib.network.client.BibLibClient
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.repository.apiCache.APICacheRepository
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
import com.pixelized.biblib.repository.book.BookRepository
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.repository.credential.CredentialRepository
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.repository.googleSignIn.GoogleSingInRepository
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.repository.user.IUserRepository
import com.pixelized.biblib.repository.user.UserRepository
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.ServiceLocator
import dagger.hilt.android.HiltAndroidApp
class BibLibApplication : Application() {
override fun onCreate() {
super.onCreate()
// Android.
ServiceLocator[BibLibDatabase::class] =
Room.databaseBuilder(this, BibLibDatabase::class.java, BibLibDatabase.DATABASE_NAME)
.fallbackToDestructiveMigration()
.build()
ServiceLocator[SharedPreferences::class] = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE)
// Web service.
ServiceLocator[Gson::class] = GsonBuilder().create()
ServiceLocator[IBibLibClient::class] = BibLibClient()
// Bitmap cache.
ServiceLocator[BitmapCache::class] = BitmapCache(this)
// Repositories.
ServiceLocator[IGoogleSingInRepository::class] = GoogleSingInRepository(this)
ServiceLocator[ICredentialRepository::class] = CredentialRepository()
ServiceLocator[IAPICacheRepository::class] = APICacheRepository()
ServiceLocator[IBookRepository::class] = BookRepository()
ServiceLocator[IUserRepository::class] = UserRepository()
}
companion object {
private const val SHARED_PREF = "BIB_LIB_SHARED_PREF"
}
}
@HiltAndroidApp
class BibLibApplication : Application()

View file

@ -0,0 +1,32 @@
package com.pixelized.biblib.module
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.pixelized.biblib.network.client.BibLibClient
import com.pixelized.biblib.network.client.IBibLibClient
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Provides
@Singleton
fun provideGsonParser(): Gson {
return GsonBuilder().create()
}
@Provides
@Singleton
fun provideBibLibClient(
gson: Gson
): IBibLibClient {
return BibLibClient(
gson = gson
)
}
}

View file

@ -0,0 +1,52 @@
package com.pixelized.biblib.module
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import com.pixelized.biblib.database.BibLibDatabase
import com.pixelized.biblib.utils.BitmapCache
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class PersistenceModule {
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context,
): BibLibDatabase {
val builder = Room.databaseBuilder(
context,
BibLibDatabase::class.java,
BibLibDatabase.DATABASE_NAME
)
return builder.fallbackToDestructiveMigration().build()
}
@Provides
@Singleton
fun provideBitmapCache(
@ApplicationContext context: Context,
): BitmapCache {
return BitmapCache(context)
}
@Provides
@Singleton
fun providePreferences(
@ApplicationContext context: Context,
): SharedPreferences {
return context.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE)
}
companion object {
private const val SHARED_PREF = "BIB_LIB_SHARED_PREF"
}
}

View file

@ -0,0 +1,80 @@
package com.pixelized.biblib.module
import android.app.Application
import android.content.SharedPreferences
import com.google.gson.Gson
import com.pixelized.biblib.database.BibLibDatabase
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.repository.apiCache.APICacheRepository
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
import com.pixelized.biblib.repository.book.BookRepository
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.repository.credential.CredentialRepository
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.repository.googleSignIn.GoogleSingInRepository
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.repository.user.IUserRepository
import com.pixelized.biblib.repository.user.UserRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class RepositoryModule {
@Provides
@Singleton
fun provideGoogleSingInRepository(
application: Application,
): IGoogleSingInRepository {
return GoogleSingInRepository(
application = application
)
}
@Provides
@Singleton
fun provideCredentialRepository(
preferences: SharedPreferences,
): ICredentialRepository {
return CredentialRepository(
preferences = preferences,
)
}
@Provides
@Singleton
fun provideAPICacheRepository(
gson: Gson,
preferences: SharedPreferences,
): IAPICacheRepository {
return APICacheRepository(
gson = gson,
preferences = preferences,
)
}
@Provides
@Singleton
fun provideBookRepository(
database: BibLibDatabase
): IBookRepository {
return BookRepository(
database = database,
)
}
@Provides
@Singleton
fun provideUserRepository(
client: IBibLibClient,
): IUserRepository {
return UserRepository(
client = client
)
}
}

View file

@ -2,15 +2,16 @@ package com.pixelized.biblib.network.client
import com.google.gson.Gson
import com.pixelized.biblib.network.client.IBibLibClient.Companion.BASE_URL
import com.pixelized.biblib.utils.injection.inject
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Inject
class BibLibClient : IBibLibClient {
private val gson by inject<Gson>()
class BibLibClient @Inject constructor(
gson: Gson,
) : IBibLibClient {
private val bearerInterceptor = BearerInterceptor()
private val httpInterceptor = HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY
@ -33,6 +34,4 @@ class BibLibClient : IBibLibClient {
set(value) {
bearerInterceptor.token = value
}
// endregion
}

View file

@ -4,11 +4,12 @@ import android.content.SharedPreferences
import androidx.core.content.edit
import com.google.gson.Gson
import com.pixelized.biblib.network.data.response.BookListResponse
import com.pixelized.biblib.utils.injection.inject
import javax.inject.Inject
class APICacheRepository : IAPICacheRepository {
private val gson: Gson by inject()
private val preferences: SharedPreferences by inject()
class APICacheRepository @Inject constructor(
private val gson: Gson,
private val preferences: SharedPreferences,
) : IAPICacheRepository {
override var new: BookListResponse?
get() = preferences.new?.let { gson.fromJson(it, BookListResponse::class.java) }

View file

@ -8,10 +8,11 @@ import com.pixelized.biblib.database.crossref.BookGenreCrossRef
import com.pixelized.biblib.database.data.*
import com.pixelized.biblib.database.relation.BookRelation
import com.pixelized.biblib.model.book.*
import com.pixelized.biblib.utils.injection.inject
import javax.inject.Inject
class BookRepository : IBookRepository {
val database: BibLibDatabase by inject()
class BookRepository @Inject constructor(
private val database: BibLibDatabase
) : IBookRepository {
override fun getAll(): List<Book> =
database.bookDao().getAll().map { it.toBook() }

View file

@ -2,11 +2,12 @@ package com.pixelized.biblib.repository.credential
import android.content.SharedPreferences
import androidx.core.content.edit
import com.pixelized.biblib.utils.injection.inject
import javax.inject.Inject
class CredentialRepository : ICredentialRepository {
private val preferences: SharedPreferences by inject()
class CredentialRepository @Inject constructor(
private val preferences: SharedPreferences,
) : ICredentialRepository {
override var login
get() = preferences.login

View file

@ -3,10 +3,11 @@ package com.pixelized.biblib.repository.user
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.UserFactory
import com.pixelized.biblib.utils.injection.inject
import javax.inject.Inject
class UserRepository : IUserRepository {
private val client: IBibLibClient by inject()
class UserRepository @Inject constructor(
private val client: IBibLibClient,
) : IUserRepository {
private var user: User? = null
override suspend fun getUser(forceUpdate: Boolean): User {

View file

@ -1,55 +1,32 @@
package com.pixelized.biblib.ui
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.ui.composable.screen.LoginScreen
import com.pixelized.biblib.ui.composable.screen.HomeScreen
import com.pixelized.biblib.ui.composable.screen.SplashScreen
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import com.pixelized.biblib.ui.navigation.FullScreenNavHost
import com.pixelized.biblib.ui.navigation.Screen
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val navigationViewModel: NavigationViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BibLibTheme {
ContentComposable()
Surface(
color = MaterialTheme.colors.background,
) {
FullScreenNavHost(
startDestination = Screen.Authentication
)
}
}
}
override fun onBackPressed() {
if (navigationViewModel.navigateBack().not()) {
super.onBackPressed()
}
}
}
@Composable
fun ContentComposable() {
val navigationViewModel = viewModel<NavigationViewModel>()
val main by navigationViewModel.screen.observeAsState()
Log.e("pouet", "Navigation State $main")
Crossfade(targetState = main) {
when (it) {
is Screen.SplashScreen -> SplashScreen()
is Screen.LoginScreen -> LoginScreen()
is Screen.MainScreen -> HomeScreen()
}
}
}

View file

@ -0,0 +1,16 @@
package com.pixelized.biblib.ui.authentication
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@Stable
class AuthenticationFormUIO(
login: State<String>,
password: State<String>,
remember: State<Boolean>,
) {
val login: String by login
val password: String by password
val remember: Boolean by remember
}

View file

@ -0,0 +1,378 @@
package com.pixelized.biblib.ui.authentication
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.sharp.Visibility
import androidx.compose.material.icons.sharp.VisibilityOff
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.pixelized.biblib.R
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.ui.composable.*
import com.pixelized.biblib.ui.navigation.LocalFullScreenNavHostController
import com.pixelized.biblib.ui.navigation.Screen
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
import com.pixelized.biblib.utils.extention.bibLib
@Composable
fun AuthenticationScreen(
viewModel: AuthenticationViewModel = hiltViewModel(),
) {
val navHostController = LocalFullScreenNavHostController.current
AuthenticationScreenContent(
login = viewModel.form.login,
password = viewModel.form.password,
rememberPassword = viewModel.form.remember,
onLoginChange = {
viewModel.onLoginChange(it)
},
onPasswordChange = {
viewModel.onPasswordChange(it)
},
onRememberPasswordChange = {
viewModel.onRememberChange(it)
},
onGoogleSignIn = {
viewModel.loginWithGoogle()
},
onSignIn = {
viewModel.login()
},
onRegister = {
navHostController.navigateToRegister()
},
)
AuthenticationHandler(
onDismissRequest = {
if (it is StateUio.Failure) viewModel.dismissError()
},
onSuccess = {
navHostController.navigateToHome()
}
)
}
@Composable
private fun AuthenticationScreenContent(
login: String,
onLoginChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit,
rememberPassword: Boolean,
onRememberPasswordChange: (Boolean) -> Unit,
onGoogleSignIn: () -> Unit,
onSignIn: () -> Unit,
onRegister: () -> Unit,
) {
val scrollState = rememberScrollState()
val focusManager = LocalFocusManager.current
AnimatedDelayer {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
) {
Spacer(modifier = Modifier.weight(1f))
AnimatedOffset(
modifier = Modifier
.padding(all = MaterialTheme.bibLib.dimen.medium)
.align(alignment = Alignment.CenterHorizontally),
) {
Text(
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.onBackground,
text = stringResource(id = R.string.authentication_title),
)
}
Spacer(modifier = Modifier.weight(1f))
AnimatedOffset(
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.medium),
) {
LoginField(
modifier = Modifier.fillMaxWidth(),
value = login,
onValueChange = onLoginChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
)
}
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.small))
AnimatedOffset(
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.medium),
) {
PasswordField(
modifier = Modifier.fillMaxWidth(),
value = password,
onValueChange = onPasswordChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { focusManager.clearFocus() },
)
}
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.medium))
AnimatedOffset(
modifier = Modifier.padding(horizontal = MaterialTheme.bibLib.dimen.medium),
) {
CredentialRemember(
value = rememberPassword,
onValueChange = onRememberPasswordChange,
)
}
Spacer(modifier = Modifier.height(MaterialTheme.bibLib.dimen.medium))
AnimatedOffset(
modifier = Modifier
.padding(horizontal = MaterialTheme.bibLib.dimen.medium)
.align(Alignment.End),
) {
Row {
Button(
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onRegister,
) {
Text(text = stringResource(id = R.string.action_register))
}
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.small))
Button(
colors = ButtonDefaults.buttonColors(),
onClick = onSignIn,
) {
Text(text = stringResource(id = R.string.action_login))
}
}
}
Spacer(modifier = Modifier.weight(2f))
AnimatedOffset {
Button(
modifier = Modifier
.padding(all = MaterialTheme.bibLib.dimen.medium)
.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onGoogleSignIn,
) {
Text(text = googleStringResource())
}
}
}
}
}
//////////////////////////////////////
// region: Authentication Handlers
@Composable
fun AuthenticationHandler(
viewModel: AuthenticationViewModel = hiltViewModel(),
onDismissRequest: (StateUio<Unit>) -> Unit = {},
onSuccess: () -> Unit = { },
) {
viewModel.PrepareLoginWithGoogle()
val state by viewModel.authenticationProcess
StateUioHandler(
state = state,
onDismissRequest = onDismissRequest,
onSuccess = onSuccess,
)
}
//////////////////////////////////////
// region: Content Helper Composable
@Composable
private fun LoginField(
modifier: Modifier = Modifier,
value: String,
onValueChange: (String) -> Unit,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
TextField(
modifier = modifier,
value = value,
onValueChange = onValueChange,
label = { Text(text = stringResource(id = R.string.authentication_login)) },
colors = TextFieldDefaults.outlinedTextFieldColors(),
maxLines = 1,
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}
@Composable
private fun PasswordField(
modifier: Modifier = Modifier,
value: String,
onValueChange: (String) -> Unit,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
var passwordVisibility by remember { mutableStateOf(false) }
TextField(
modifier = modifier,
value = value,
onValueChange = onValueChange,
label = { Text(text = stringResource(id = R.string.authentication_password)) },
colors = TextFieldDefaults.outlinedTextFieldColors(),
maxLines = 1,
singleLine = true,
visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { passwordVisibility = passwordVisibility.not() }) {
Icon(
imageVector = if (passwordVisibility) Icons.Sharp.VisibilityOff else Icons.Sharp.Visibility,
contentDescription = null,
)
}
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}
@Composable
private fun CredentialRemember(
modifier: Modifier = Modifier,
value: Boolean,
onValueChange: (Boolean) -> Unit,
) {
Row(
modifier = modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onValueChange(value.not()) }
)
) {
Checkbox(
modifier = Modifier.align(Alignment.CenterVertically),
checked = value,
onCheckedChange = null
)
Spacer(modifier = Modifier.width(MaterialTheme.bibLib.dimen.small))
Text(
modifier = Modifier.align(Alignment.CenterVertically),
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onBackground,
text = stringResource(id = R.string.authentication_credential_remember)
)
}
}
// endregion
//////////////////////////////////////
// region: Navigation Helper
private fun NavHostController.navigateToHome() {
navigate(Screen.Home.route) { popUpTo(0) { inclusive = true } }
}
private fun NavHostController.navigateToRegister() {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.REGISTER_URL)))
}
// endregion
//////////////////////////////////////
// region: AnnotatedString
@Composable
@ReadOnlyComposable
fun googleStringResource(): AnnotatedString = buildAnnotatedString {
val default = LocalTextStyle.current.toSpanStyle()
withStyle(
style = default
) {
append(stringResource(id = R.string.action_google_sign_in))
append(" ")
}
withStyle(
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
) {
append("G")
}
withStyle(
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
) {
append("o")
}
withStyle(
style = default.copy(color = GoogleColorPalette.yellow, fontWeight = FontWeight.ExtraBold),
) {
append("o")
}
withStyle(
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
) {
append("g")
}
withStyle(
style = default.copy(color = GoogleColorPalette.green, fontWeight = FontWeight.ExtraBold),
) {
append("l")
}
withStyle(
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
) {
append("e")
}
}
@Composable
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun AuthenticationScreenContentPreview() {
BibLibTheme {
AuthenticationScreenContent(
login = "",
onLoginChange = { },
password = "",
onPasswordChange = { },
rememberPassword = true,
onRememberPasswordChange = { },
onGoogleSignIn = { },
onSignIn = { },
onRegister = { },
)
}
}

View file

@ -0,0 +1,220 @@
package com.pixelized.biblib.ui.authentication
import android.content.Intent
import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.common.api.ApiException
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.data.query.AuthLoginQuery
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
import com.pixelized.biblib.utils.exception.MissingTokenException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AuthenticationViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
private val googleSignIn: IGoogleSingInRepository,
private val client: IBibLibClient,
) : ViewModel() {
private var launcher: ActivityResultLauncher<Intent>? = null
private var authenticationJob: Job? = null
private val _authenticationProcess = mutableStateOf<StateUio<Unit>?>(null)
val authenticationProcess: State<StateUio<Unit>?> get() = _authenticationProcess
private val _login: MutableState<String>
private val _password: MutableState<String>
private val _remember = mutableStateOf(credentialRepository.rememberCredential)
val form: AuthenticationFormUIO
get() = AuthenticationFormUIO(
login = _login,
password = _password,
remember = _remember,
)
init {
if (credentialRepository.rememberCredential) {
_login = mutableStateOf(credentialRepository.login ?: "")
_password = mutableStateOf(credentialRepository.password ?: "")
} else {
_login = mutableStateOf("")
_password = mutableStateOf("")
}
}
//////////////////////////////////////
// region: Login with BibLibClient
fun login(
login: String = _login.value,
password: String = _password.value,
) {
authenticationJob?.cancel()
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
val query = AuthLoginQuery(username = login, password = password)
_authenticationProcess.value = StateUio.Progress()
try {
val response = client.service.login(query)
val idToken = response.token ?: throw MissingTokenException()
client.token = idToken
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
// endregion
//////////////////////////////////////
// region: Login with Google
@Composable
fun PrepareLoginWithGoogle() {
launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
authenticationJob?.cancel()
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
try {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account = task.getResult(ApiException::class.java)
val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
val response = client.service.loginWithGoogle(googleToken)
val token = response.token ?: throw MissingTokenException()
client.token = token
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
}
fun loginWithGoogle() {
_authenticationProcess.value = StateUio.Progress()
launcher?.launch(googleSignIn.client.signInIntent)
}
// endregion
//////////////////////////////////////
// region: AutoLogin
@Composable
fun AutoLogin() {
LaunchedEffect(key1 = "AuthenticationViewModel AutoLogin") {
authenticationJob?.cancel()
authenticationJob = launch(Dispatchers.IO) {
_authenticationProcess.value = StateUio.Progress()
try {
autoLoginWithGoogle() || autologinWithCredential()
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
}
private suspend fun autoLoginWithGoogle(): Boolean {
val googleToken = googleSignIn.lastGoogleToken
return if (googleToken != null) {
try {
client.service.loginWithGoogle(googleToken).let { response ->
if (response.token != null) {
client.token = response.token
true
} else {
false
}
}
} catch (e: Exception) {
false
}
} else {
false
}
}
private suspend fun autologinWithCredential(): Boolean {
val login = credentialRepository.login
val password = credentialRepository.password
return if (login != null && password != null) {
try {
val query = AuthLoginQuery(login, password)
client.service.login(query).let { response ->
if (response.token != null) {
client.token = response.token
true
} else {
false
}
}
} catch (e: Exception) {
false
}
} else {
false
}
}
// endregion
//////////////////////////////////////
// region: OnDataChange callback.
fun onLoginChange(login: String) {
// update login in the repository
if (_remember.value) {
credentialRepository.login = login
}
// update the UI State
_login.value = login
}
fun onPasswordChange(password: String) {
// update password in the repository
if (_remember.value) {
credentialRepository.password = password
}
// update the UI State
_password.value = password
}
fun onRememberChange(remember: Boolean) {
// save the remember state in the repository
credentialRepository.rememberCredential = remember
// update login & password in the repository
if (remember.not()) {
credentialRepository.login = null
credentialRepository.password = null
} else {
credentialRepository.login = _login.value
credentialRepository.password = _password.value
}
// update the UI State
_remember.value = remember
}
// endregion
//////////////////////////////////////
// region: Dialog
fun dismissError() {
_authenticationProcess.value = null
}
}

View file

@ -0,0 +1,15 @@
package com.pixelized.biblib.ui.composable
import androidx.compose.runtime.Composable
@Composable
fun AnimatedDelayer(
content: @Composable AnimatedDelayerScope.() -> Unit
) {
val scope = AnimatedDelayerScope()
scope.content()
}
class AnimatedDelayerScope(
var delay : Delay = Delay()
)

View file

@ -0,0 +1,146 @@
package com.pixelized.biblib.ui.composable
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun AnimatedDelayerScope.AnimatedOffset(
modifier: Modifier = Modifier,
transitionLabel: String = "AnimatedOffset",
content: @Composable BoxScope.() -> Unit,
) {
AnimatedOffset(
modifier = modifier,
transitionLabel = transitionLabel,
delay = delay++,
content = content,
)
}
@Composable
fun AnimatedOffset(
modifier: Modifier = Modifier,
transitionLabel: String = "AnimatedOffset",
delay: Delay = Delay(),
content: @Composable BoxScope.() -> Unit,
) {
val displayed = rememberSavableMutableTransitionState(
initialState = LocalInspectionMode.current,
targetState = true,
)
AnimatedOffset(
modifier = modifier,
transitionLabel = transitionLabel,
displayed = displayed,
delay = delay,
content = content,
)
}
@Composable
fun AnimatedOffset(
modifier: Modifier = Modifier,
displayed: MutableTransitionState<Boolean>,
transitionLabel: String = "AnimatedOffset",
delay: Delay = Delay(),
content: @Composable BoxScope.() -> Unit,
) {
val transition: TransitionData = updateTransition(
displayed = displayed,
label = transitionLabel,
delay = delay.value,
)
Box(
modifier = modifier
.offset(y = transition.offset)
.graphicsLayer(alpha = transition.alpha, clip = false),
content = content
)
}
@Composable
private fun updateTransition(
displayed: MutableTransitionState<Boolean>,
label: String,
duration: Int = 600,
delay: Int = 0,
easing: CubicBezierEasing = CubicBezierEasing(0.25f, 1f, 0.5f, 1f),
fromAlpha: Float = 0f,
toAlpha: Float = 1f,
fromOffset: Dp = 64.dp,
toOffset: Dp = 0.dp,
): TransitionData {
val transition = updateTransition(transitionState = displayed, label = label)
val alpha = transition.animateFloat(
label = "$label :: alpha",
transitionSpec = { tween(durationMillis = duration, easing = easing, delayMillis = delay) },
) {
when (it) {
true -> toAlpha
else -> fromAlpha
}
}
val offset = transition.animateDp(
label = "$label :: offset",
transitionSpec = { tween(durationMillis = duration, easing = easing, delayMillis = delay) },
) {
when (it) {
true -> toOffset
else -> fromOffset
}
}
return remember(transition) { TransitionData(alpha, offset) }
}
private class TransitionData(
alpha: State<Float>,
offset: State<Dp>,
) {
val alpha by alpha
val offset by offset
}
@JvmInline
value class Delay(
val value: Int = DELTA,
) {
operator fun inc(): Delay {
return Delay(value = value + DELTA)
}
companion object {
private const val DELTA = 100
}
}
@Composable
private inline fun <reified T> rememberSavableMutableTransitionState(
initialState: T, targetState: T
) = rememberSaveable(saver = saver()) {
MutableTransitionState(initialState).apply { this.targetState = targetState }
}
private inline fun <reified T> saver() = object : Saver<MutableTransitionState<T>, Pair<T, T>> {
override fun restore(value: Pair<T, T>): MutableTransitionState<T> {
return MutableTransitionState(value.first).apply { targetState = value.second }
}
override fun SaverScope.save(value: MutableTransitionState<T>): Pair<T, T> {
return value.currentState to value.targetState
}
}

View file

@ -0,0 +1,74 @@
package com.pixelized.biblib.ui.composable
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Dialog
import com.pixelized.biblib.ui.old.composable.dialog.ErrorCard
import com.pixelized.biblib.ui.old.composable.dialog.LoadingCard
sealed class StateUio<T> {
class Progress<T>(val progress: Float? = null) : StateUio<T>()
class Failure<T>(val exception: Exception) : StateUio<T>()
class Success<T>(val value: T) : StateUio<T>()
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun <T> StateUioHandler(
state: StateUio<T>?,
onDismissRequest: (StateUio<T>) -> Unit = {},
onSuccess: () -> Unit = { },
) {
val currentOnDismissRequest by rememberUpdatedState(onDismissRequest)
val currentOnSuccess by rememberUpdatedState(onSuccess)
when (state) {
is StateUio.Progress,
is StateUio.Failure -> {
Dialog(onDismissRequest = { currentOnDismissRequest(state) }) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onDismissRequest(state) }
),
contentAlignment = Alignment.Center
) {
AnimatedContent(
targetState = state,
contentAlignment = Alignment.Center,
transitionSpec = {
fadeIn(tween(delayMillis = 150)) + scaleIn(
initialScale = 0.85f,
animationSpec = tween(delayMillis = 150)
) with fadeOut(tween()) + scaleOut(
targetScale = 0.85f,
animationSpec = tween()
)
},
) {
when (it) {
is StateUio.Progress -> LoadingCard()
is StateUio.Failure -> ErrorCard(exception = it.exception)
else -> Unit // nothing to do.
}
}
}
}
}
is StateUio.Success -> currentOnSuccess()
null -> Unit // nothing to do.
}
}

View file

@ -0,0 +1,34 @@
package com.pixelized.biblib.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.pixelized.biblib.ui.authentication.AuthenticationScreen
val LocalFullScreenNavHostController = compositionLocalOf<NavHostController> {
error("LocalFullScreenNavHostController is not ready yet.")
}
@Composable
fun FullScreenNavHost(
navHostController: NavHostController = rememberNavController(),
startDestination: Screen = Screen.Authentication
) {
CompositionLocalProvider(LocalFullScreenNavHostController provides navHostController) {
NavHost(
navController = navHostController,
startDestination = startDestination.route,
) {
composable(Screen.Authentication.route) {
AuthenticationScreen()
}
composable(Screen.Home.route) {
}
}
}
}

View file

@ -0,0 +1,12 @@
package com.pixelized.biblib.ui.navigation
sealed class Screen(
val route: String,
) {
object Authentication : Screen(
route = "authentication"
)
object Home : Screen(
route = "home"
)
}

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.common
package com.pixelized.biblib.ui.old.composable.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -22,7 +22,7 @@ import com.pixelized.biblib.BuildConfig
import com.pixelized.biblib.R
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.old.viewmodel.user.IUserViewModel
import java.util.*

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.common
package com.pixelized.biblib.ui.old.composable.common
import androidx.compose.animation.Crossfade
import androidx.compose.material.Icon

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.common
package com.pixelized.biblib.ui.old.composable.common
import android.widget.TextView
import androidx.compose.runtime.Composable

View file

@ -1,5 +1,6 @@
package com.pixelized.biblib.ui.composable.common
package com.pixelized.biblib.ui.old.composable.common
import android.content.Context
import androidx.compose.animation.Crossfade
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -10,8 +11,9 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.get
import androidx.compose.ui.platform.LocalContext
import com.pixelized.biblib.module.PersistenceModule
import dagger.hilt.android.EntryPointAccessors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.net.URL
@ -29,8 +31,9 @@ fun Image(
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val cover by remember { download(placeHolder, coroutineScope, contentUrl) }
val cover by remember { download(context, placeHolder, coroutineScope, contentUrl) }
Crossfade(modifier = modifier, targetState = cover) {
if (it == placeHolder) {
@ -57,14 +60,17 @@ fun Image(
}
private fun download(
context: Context,
placeHolder: Painter,
coroutineScope: CoroutineScope,
url: URL,
): State<Painter> {
val state = mutableStateOf(placeHolder)
val cache = EntryPointAccessors
.fromApplication(context, PersistenceModule::class.java)
.provideBitmapCache(context)
coroutineScope.launch {
val cache: BitmapCache = get()
val resource = cache.readFromDisk(url)?.let { BitmapPainter(it.asImageBitmap()) }
if (resource != null) {
state.value = resource

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.dialog
package com.pixelized.biblib.ui.old.composable.dialog
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.dialog
package com.pixelized.biblib.ui.old.composable.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
@ -44,19 +44,20 @@ fun ErrorCard(
imageVector = Icons.Sharp.ErrorOutline,
contentDescription = "error"
)
val typography = MaterialTheme.typography
if (message.isNotEmpty()) {
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
style = typography.body1,
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
text = message
)
}
if (exception != null) {
Text(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.CenterHorizontally),
style = typography.caption,
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = exception.message ?: exception::class.java.simpleName
)

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.dialog
package com.pixelized.biblib.ui.old.composable.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -16,12 +16,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
@Composable
fun LoadingCard(
modifier: Modifier = Modifier,
message: String? = null
progress: Float? = null,
message: String? = null,
) {
Card(elevation = 8.dp, modifier = modifier) {
Column(
@ -29,11 +31,20 @@ fun LoadingCard(
.width(250.dp)
.padding(32.dp)
) {
if (progress == null) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp)
.padding(bottom = MaterialTheme.bibLib.dimen.medium)
)
} else {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = MaterialTheme.bibLib.dimen.medium),
progress = progress,
)
}
if (message?.isNotEmpty() == true) {
val typography = MaterialTheme.typography
Text(

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.dialog
package com.pixelized.biblib.ui.old.composable.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.items
package com.pixelized.biblib.ui.old.composable.items
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -12,19 +12,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.common.Image
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.old.composable.common.Image
import com.pixelized.biblib.ui.old.data.BookThumbnailUio
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.Teal200
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.ServiceLocator
import com.pixelized.biblib.ui.theme.color.BibLibColorPalette
import com.pixelized.biblib.utils.mock.BookThumbnailMock
private val THUMBNAIL_WIDTH: Dp = 60.dp
@ -68,7 +65,7 @@ private fun FilledBookItem(
placeHolder = painterResource(id = R.drawable.ic_launcher_foreground),
contentScale = ContentScale.FillBounds,
contentUrl = thumbnail.cover,
colorFilter = if (MaterialTheme.colors.isLight) ColorFilter.tint(Teal200) else null,
colorFilter = if (MaterialTheme.colors.isLight) ColorFilter.tint(BibLibColorPalette.Green) else null,
contentDescription = thumbnail.title
)
Column(
@ -158,7 +155,6 @@ private fun Placeholder(modifier: Modifier) = Surface(modifier = modifier, eleva
@Preview
@Composable
fun BookItemLightPreview() {
ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme {
val mock = BookThumbnailMock()
FilledBookItem(thumbnail = mock.bookThumbnail)
@ -168,7 +164,6 @@ fun BookItemLightPreview() {
@Preview
@Composable
fun BookItemDarkPreview() {
ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme(darkTheme = true) {
val mock = BookThumbnailMock()
FilledBookItem(thumbnail = mock.bookThumbnail)

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.pages
package com.pixelized.biblib.ui.old.composable.pages
import android.widget.Toast
import androidx.compose.animation.ExperimentalAnimationApi
@ -32,18 +32,16 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.common.HtmlText
import com.pixelized.biblib.ui.composable.common.Image
import com.pixelized.biblib.ui.data.BookUio
import com.pixelized.biblib.ui.data.MailUio
import com.pixelized.biblib.ui.old.composable.common.HtmlText
import com.pixelized.biblib.ui.old.composable.common.Image
import com.pixelized.biblib.ui.old.data.BookUio
import com.pixelized.biblib.ui.old.data.MailUio
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.Teal200
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.viewmodel.user.UserViewModel
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.ServiceLocator
import com.pixelized.biblib.ui.theme.color.BibLibColorPalette
import com.pixelized.biblib.ui.old.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.old.viewmodel.user.UserViewModel
import com.pixelized.biblib.utils.mock.BookMock
import kotlinx.coroutines.launch
@ -134,7 +132,7 @@ fun DetailPage(
contentScale = ContentScale.FillWidth,
placeHolder = painterResource(id = R.drawable.ic_launcher_foreground),
contentUrl = book.cover,
colorFilter = if (MaterialTheme.colors.isLight) ColorFilter.tint(Teal200) else null,
colorFilter = if (MaterialTheme.colors.isLight) ColorFilter.tint(BibLibColorPalette.Green) else null,
contentDescription = book.title
)
Row(modifier = Modifier.padding(bottom = 16.dp)) {
@ -296,7 +294,6 @@ private fun SendMailItem(
@Preview
@Composable
fun DetailPageLightPreview() {
ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme {
val book = BookMock()
DetailPage(book.book)

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.pages
package com.pixelized.biblib.ui.old.composable.pages
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
@ -13,11 +13,11 @@ import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.pixelized.biblib.ui.composable.items.BookItem
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.old.composable.items.BookItem
import com.pixelized.biblib.ui.old.data.BookThumbnailUio
import com.pixelized.biblib.ui.old.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel.Navigable.Page
@Composable

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.screen
package com.pixelized.biblib.ui.old.composable.screen
import android.widget.Toast
import androidx.compose.animation.*
@ -12,19 +12,19 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.common.BibLibDrawer
import com.pixelized.biblib.ui.composable.common.BibLibToolbar
import com.pixelized.biblib.ui.composable.pages.DetailPage
import com.pixelized.biblib.ui.composable.pages.HomePage
import com.pixelized.biblib.ui.theme.Animation
import com.pixelized.biblib.ui.old.composable.common.BibLibDrawer
import com.pixelized.biblib.ui.old.composable.common.BibLibToolbar
import com.pixelized.biblib.ui.old.composable.pages.DetailPage
import com.pixelized.biblib.ui.old.composable.pages.HomePage
import com.pixelized.biblib.ui.theme.animation.Animation
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.viewmodel.user.UserViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.old.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.ui.old.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.old.viewmodel.user.UserViewModel
import kotlinx.coroutines.launch

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.screen
package com.pixelized.biblib.ui.old.composable.screen
import android.content.Intent
@ -37,19 +37,19 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.network.client.IBibLibClient.Companion.REGISTER_URL
import com.pixelized.biblib.ui.composable.dialog.ErrorCard
import com.pixelized.biblib.ui.composable.dialog.LoadingCard
import com.pixelized.biblib.ui.composable.dialog.SuccessCard
import com.pixelized.biblib.ui.old.composable.dialog.ErrorCard
import com.pixelized.biblib.ui.old.composable.dialog.LoadingCard
import com.pixelized.biblib.ui.old.composable.dialog.SuccessCard
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.credential.CredentialViewModel
import com.pixelized.biblib.ui.viewmodel.credential.ICredentialViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.ui.old.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.old.viewmodel.authentication.IAuthenticationViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.credential.CredentialViewModel
import com.pixelized.biblib.ui.old.viewmodel.credential.ICredentialViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import com.pixelized.biblib.ui.old.viewmodel.navigation.NavigationViewModel
import kotlinx.coroutines.delay
@ -243,7 +243,8 @@ private fun SignIn(
colors = outlinedButtonColors(),
onClick = {
authenticationViewModel.loginWithGoogle()
}) {
}
) {
Image(
modifier = Modifier.padding(end = 8.dp),
painter = painterResource(id = R.drawable.ic_google), contentDescription = ""
@ -329,8 +330,8 @@ private fun CredentialRemember(
}
}
@Preview
@Composable
@Preview(showBackground = true)
fun LoginScreenComposablePreview() {
BibLibTheme {
LoginScreen(

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.screen
package com.pixelized.biblib.ui.old.composable.screen
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
@ -17,19 +17,19 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.BuildConfig
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.dialog.CrossFadeOverlay
import com.pixelized.biblib.ui.composable.dialog.ErrorCard
import com.pixelized.biblib.ui.theme.Animation
import com.pixelized.biblib.ui.old.composable.dialog.CrossFadeOverlay
import com.pixelized.biblib.ui.old.composable.dialog.ErrorCard
import com.pixelized.biblib.ui.theme.animation.Animation
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.viewmodel.user.UserViewModel
import com.pixelized.biblib.ui.old.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.old.viewmodel.authentication.IAuthenticationViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import com.pixelized.biblib.ui.old.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.ui.old.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.old.viewmodel.user.UserViewModel
import kotlinx.coroutines.delay
import java.util.*
@ -184,20 +184,12 @@ private fun AuthenticationError(state: IAuthenticationViewModel.State.Error?) {
CrossFadeOverlay(
modifier = Modifier.clickable {},
visible = state != null
) {
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = true,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
ErrorCard(
message = stringResource(id = R.string.error_generic),
exception = state?.exception
)
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@ -206,20 +198,12 @@ private fun BookError(state: IBooksViewModel.State.Error?) {
CrossFadeOverlay(
modifier = Modifier.clickable {},
visible = state != null
) {
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = true,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
ErrorCard(
message = stringResource(id = R.string.error_generic),
exception = state?.exception
)
}
}
}
@Composable

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.data
package com.pixelized.biblib.ui.old.data
import com.pixelized.biblib.network.client.IBibLibClient.Companion.THUMBNAIL_URL
import java.net.URL

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.data
package com.pixelized.biblib.ui.old.data
import com.pixelized.biblib.network.client.IBibLibClient.Companion.COVER_URL
import java.net.URL

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.data
package com.pixelized.biblib.ui.old.data
data class MailUio(
val mail: String,

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.viewmodel.authentication
package com.pixelized.biblib.ui.old.viewmodel.authentication
import android.content.Intent
import android.util.Log
@ -16,17 +16,20 @@ import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.data.query.AuthLoginQuery
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel.State
import com.pixelized.biblib.ui.old.viewmodel.authentication.IAuthenticationViewModel.State
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
import com.pixelized.biblib.utils.exception.MissingTokenException
import com.pixelized.biblib.utils.injection.inject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class AuthenticationViewModel : ViewModel(), IAuthenticationViewModel {
private val credentialRepository: ICredentialRepository by inject()
private val googleSignIn: IGoogleSingInRepository by inject()
private val client: IBibLibClient by inject()
@HiltViewModel
class AuthenticationViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
private val googleSignIn: IGoogleSingInRepository,
private val client: IBibLibClient,
) : ViewModel(), IAuthenticationViewModel {
private var launcher: ActivityResultLauncher<Intent>? = null
private val _state = MutableLiveData<State>(State.Initial)

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.viewmodel.authentication
package com.pixelized.biblib.ui.old.viewmodel.authentication
import androidx.compose.runtime.Composable
import androidx.lifecycle.LiveData

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.viewmodel.book
package com.pixelized.biblib.ui.old.viewmodel.book
import android.util.Log
import androidx.lifecycle.LiveData
@ -14,23 +14,26 @@ import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.data.BookUio
import com.pixelized.biblib.utils.injection.inject
import com.pixelized.biblib.ui.old.data.BookThumbnailUio
import com.pixelized.biblib.ui.old.data.BookUio
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
/**
* TODO: there is some book related code that should be inside a Repository // DataSource.
*/
class BooksViewModel : ViewModel(), IBooksViewModel {
private val bookRepository: IBookRepository by inject()
private val client: IBibLibClient by inject()
private val apiCache: IAPICacheRepository by inject()
@HiltViewModel
class BooksViewModel @Inject constructor(
private val bookRepository: IBookRepository,
private val client: IBibLibClient,
private val apiCache: IAPICacheRepository,
) : ViewModel(), IBooksViewModel {
private val formatterLong = SimpleDateFormat("MMMM yyyy", Locale.getDefault())
private val formatterShort = SimpleDateFormat("MMM yyyy", Locale.getDefault())
@ -122,7 +125,7 @@ class BooksViewModel : ViewModel(), IBooksViewModel {
date = if (releaseDate.time < 0) {
null
} else {
formatterLong.format(releaseDate).capitalize(Locale.getDefault())
formatterLong.format(releaseDate).capitalize()
},
isNew = isNew,
)
@ -132,16 +135,20 @@ class BooksViewModel : ViewModel(), IBooksViewModel {
title = title,
author = author.joinToString { it.name },
rating = rating?.toFloat() ?: 0.0f,
language = language?.displayLanguage?.capitalize(Locale.getDefault()) ?: "",
language = language?.displayLanguage?.capitalize() ?: "",
date = if (releaseDate.time < 0) {
null
} else {
formatterShort.format(releaseDate).capitalize(Locale.getDefault())
formatterShort.format(releaseDate).capitalize()
},
series = series?.name,
description = synopsis ?: "",
)
private fun String.capitalize() = this.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
companion object {
private const val PAGING_SIZE = 30
}

View file

@ -1,10 +1,10 @@
package com.pixelized.biblib.ui.viewmodel.book
package com.pixelized.biblib.ui.old.viewmodel.book
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagingData
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.data.BookUio
import com.pixelized.biblib.ui.old.data.BookThumbnailUio
import com.pixelized.biblib.ui.old.data.BookUio
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

View file

@ -1,16 +1,19 @@
package com.pixelized.biblib.ui.viewmodel.credential
package com.pixelized.biblib.ui.old.viewmodel.credential
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.utils.injection.inject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class CredentialViewModel : ViewModel(), ICredentialViewModel {
private val credentialRepository: ICredentialRepository by inject()
@HiltViewModel
class CredentialViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
) : ViewModel(), ICredentialViewModel {
private val _login = MutableLiveData<String>()
override val login: LiveData<String?> get() = _login

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.viewmodel.credential
package com.pixelized.biblib.ui.old.viewmodel.credential
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.viewmodel.navigation
package com.pixelized.biblib.ui.old.viewmodel.navigation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData

View file

@ -1,11 +1,11 @@
package com.pixelized.biblib.ui.viewmodel.navigation
package com.pixelized.biblib.ui.old.viewmodel.navigation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel.Navigable
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.old.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import java.util.*
class NavigationViewModel : ViewModel(), INavigationViewModel {

View file

@ -1,12 +1,9 @@
package com.pixelized.biblib.ui.viewmodel.user
package com.pixelized.biblib.ui.old.viewmodel.user
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.pixelized.biblib.R
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.ui.viewmodel.credential.ICredentialViewModel
import com.pixelized.biblib.utils.mock.UserMock
import java.util.*
interface IUserViewModel {
val state: LiveData<State>

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.viewmodel.user
package com.pixelized.biblib.ui.old.viewmodel.user
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -6,14 +6,16 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.repository.user.IUserRepository
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel.State
import com.pixelized.biblib.utils.injection.inject
import com.pixelized.biblib.ui.old.viewmodel.user.IUserViewModel.State
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class UserViewModel : ViewModel(), IUserViewModel {
private val userRepository: IUserRepository by inject()
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: IUserRepository,
) : ViewModel(), IUserViewModel {
private val _state = MutableLiveData<State>()
override val state: LiveData<State> get() = _state

View file

@ -0,0 +1,18 @@
package com.pixelized.biblib.ui.theme
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import com.pixelized.biblib.ui.theme.color.BibLibColor
import com.pixelized.biblib.ui.theme.dimen.BibLibDimen
val LocalBibLibTheme = compositionLocalOf<BibLibTheme> {
error("BibLibTheme not ready yet.")
}
@Stable
@Immutable
data class BibLibTheme(
val dimen: BibLibDimen,
val color: BibLibColor,
)

View file

@ -1,12 +0,0 @@
package com.pixelized.biblib.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val Green600 = Color(0xFF43a047)
val Green600L = Color(0xFF76d275)
val Green600D = Color(0xFF00701a)

View file

@ -2,39 +2,29 @@ package com.pixelized.biblib.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val DarkColorPalette = darkColors(
primary = Green600,
primaryVariant = Green600D,
onPrimary = Color.White,
)
private val LightColorPalette = lightColors(
primary = Green600,
primaryVariant = Green600D,
onPrimary = Color.White,
)
import androidx.compose.runtime.CompositionLocalProvider
import com.pixelized.biblib.ui.theme.color.bibLibDarkColors
import com.pixelized.biblib.ui.theme.color.bibLibLightColors
import com.pixelized.biblib.ui.theme.dimen.BibLibDimen
import com.pixelized.biblib.ui.theme.shape.Shapes
import com.pixelized.biblib.ui.theme.typography.Typography
@Composable
fun BibLibTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
fun BibLibTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val theme = BibLibTheme(
dimen = BibLibDimen(),
color = if (darkTheme) bibLibDarkColors() else bibLibLightColors()
)
CompositionLocalProvider(LocalBibLibTheme provides theme) {
MaterialTheme(
colors = colors,
colors = theme.color.base,
typography = Typography,
shapes = Shapes,
) {
Surface(
color = MaterialTheme.colors.background,
content = content
)
}

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.theme
package com.pixelized.biblib.ui.theme.animation
object Animation {

View file

@ -0,0 +1,39 @@
package com.pixelized.biblib.ui.theme.color
import androidx.compose.material.Colors
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color
import javax.annotation.concurrent.Immutable
@Stable
@Immutable
data class BibLibColor(
val base: Colors
)
fun bibLibDarkColors(
base: Colors = darkColors(
primary = BibLibColorPalette.Green,
secondary = BibLibColorPalette.VeryLightGreen,
onPrimary = Color.White,
onSecondary = Color.White,
error = BibLibColorPalette.Red,
),
) = BibLibColor(
base = base,
)
fun bibLibLightColors(
base: Colors = lightColors(
primary = BibLibColorPalette.Green,
secondary = BibLibColorPalette.VeryLightGreen,
onPrimary = Color.White,
onSecondary = Color.White,
error = BibLibColorPalette.Red,
)
) = BibLibColor(
base = base,
)

View file

@ -0,0 +1,52 @@
package com.pixelized.biblib.ui.theme.color
import androidx.compose.ui.graphics.Color
import javax.annotation.concurrent.Immutable
@Immutable
object BibLibColorPalette {
val VeryDarkBlue: Color = Color(0xFF09179D)
val DarkBlue: Color = Color(0xFF1A2BDB)
val Blue: Color = Color(0xFF2970F2)
val LightBlue: Color = Color(0xFF1A91DB)
val VeryLightBlue: Color = Color(0xFF1EDDEF)
val VeryDarkPurple: Color = Color(0xFF5F0E9E)
val DarkPurple: Color = Color(0xFF8330DB)
val Purple: Color = Color(0xFF9B54C3)
val LightPurple: Color = Color(0xFFBC52D9)
val VeryLightPurple: Color = Color(0xFFC856D1)
val VeryDarkGreen: Color = Color(0xFF16544A)
val DarkGreen: Color = Color(0xFF207A6B)
val Green: Color = Color(0xFF269482)
val LightGreen: Color = Color(0xFF2AA18D)
val VeryLightGreen: Color = Color(0xFF3AE0C5)
val VeryDarkRed: Color = Color(0xFF631221)
val DarkRed: Color = Color(0xFFA21D36)
val Red: Color = Color(0xFFC92443)
val LightRed: Color = Color(0xFFE32849)
val VeryLightRed: Color = Color(0xFFF02B4F)
val VeryDarkPink: Color = Color(0xFF960064)
val DarkPink: Color = Color(0xFFBD007E)
val Pink: Color = Color(0xFFD6008F)
val LightPink: Color = Color(0xFFE35BB5)
val VeryLightPink: Color = Color(0xFFFF66CC)
val VeryDarkYellow: Color = Color(0xFFB76036)
val DarkYellow: Color = Color(0xFFD48341)
val Yellow: Color = Color(0xFFF3A850)
val LightYellow: Color = Color(0xFFF5BF63)
val VeryLightYellow: Color = Color(0xFFF9D679)
val VeryDarkGrey: Color = Color(0xFF1D1D1D)
val DarkGrey: Color = Color(0xFF727272)
val Grey: Color = Color(0xFF919195)
val LightGrey: Color = Color(0xFFDFDFDF)
val VeryLightGrey: Color = Color(0xFFF9F9F9)
}
@Immutable
object GoogleColorPalette {
val blue: Color = Color(0xFF4285F4)
val red: Color = Color(0xFFEA4335)
val yellow: Color = Color(0xFFFBBC05)
val green: Color = Color(0xFF34A853)
}

View file

@ -0,0 +1,17 @@
package com.pixelized.biblib.ui.theme.dimen
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Stable
@Immutable
data class BibLibDimen(
val default: Dp = 0.dp,
val extraSmall: Dp = 4.dp,
val small: Dp = 8.dp,
val medium: Dp = 16.dp,
val large: Dp = 32.dp,
val extraLarge: Dp = 64.dp,
)

View file

@ -1,11 +1,11 @@
package com.pixelized.biblib.ui.theme
package com.pixelized.biblib.ui.theme.shape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
small = RoundedCornerShape(50),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.theme
package com.pixelized.biblib.ui.theme.typography
import androidx.compose.material.Typography

View file

@ -0,0 +1,7 @@
package com.pixelized.biblib.utils.extention
import android.content.Context
import android.widget.Toast
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
Toast.makeText(this, message, duration).show()

View file

@ -0,0 +1,12 @@
package com.pixelized.biblib.utils.extention
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.LocalBibLibTheme
val MaterialTheme.bibLib : BibLibTheme
@Composable
@ReadOnlyComposable
get() = LocalBibLibTheme.current

View file

@ -1,21 +0,0 @@
package com.pixelized.biblib.utils.injection
import com.pixelized.biblib.utils.exception.InjectionException
import kotlin.reflect.KClass
@Suppress("UNCHECKED_CAST")
object ServiceLocator {
private val components = hashMapOf<KClass<*>, Any>()
operator fun <I : Any, O : I> set(clazz: KClass<I>, component: O) {
components[clazz] = component
}
operator fun <T> get(clazz: KClass<*>): T {
return components[clazz] as? T ?: throw InjectionException(clazz)
}
}
inline fun <reified T> get(): T = ServiceLocator[T::class]
inline fun <reified T> inject(): Lazy<T> = lazy { ServiceLocator[T::class] }

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.utils.mock
import com.pixelized.biblib.ui.data.BookUio
import com.pixelized.biblib.ui.old.data.BookUio
class BookMock {
val book: BookUio = BookUio(

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.utils.mock
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.old.data.BookThumbnailUio
class BookThumbnailMock {

View file

@ -9,7 +9,7 @@
<string name="action_epub">EPUB</string>
<string name="action_mobi">MOBI</string>
<string name="action_send">SEND</string>
<string name="action_google_sign_in">Sign in with Google</string>
<string name="action_google_sign_in">Sign in with</string>
<!-- Dialogs -->

View file

@ -1,15 +1,18 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
kotlin_version = "1.6.10"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.10"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
dependencies {
classpath 'com.android.tools.build:gradle:7.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:2.40.5"
}
}

View file

@ -1,6 +1,6 @@
#Wed Apr 28 21:29:33 CEST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME