1
0
mirror of https://github.com/foobnix/LibreraReader.git synced 2025-10-06 00:02:43 +02:00

refactoring

This commit is contained in:
Ivan Ivanenko
2025-08-06 13:55:49 +03:00
parent ea7cfff09e
commit e546e0f8b9
15 changed files with 510 additions and 63 deletions

View File

@@ -81,6 +81,10 @@ android {
debuggable = true
}
}
buildFeatures {
compose true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
@@ -287,6 +291,10 @@ dependencies {
fdroidImplementation project(':pro')
implementation project(':smartreflow')
//compose
implementation 'androidx.compose.ui:ui'
implementation project(":googleDrive")
/** AndroidX **/
implementation 'androidx.cardview:cardview:1.0.0'
@@ -455,7 +463,7 @@ task incVersion() {
if (!taskName.contains("Fdroid")) {
dependencies {
implementation "com.github.junrar:junrar:4.0.0"
implementation project(":googleDrive")
}
}

View File

@@ -31,6 +31,7 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.compose.ui.platform.ComposeView;
import androidx.core.graphics.ColorUtils;
import androidx.core.util.Pair;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -94,6 +95,8 @@ import java.util.Locale;
import java.util.Set;
import java.util.Stack;
import mobi.librera.lib.gdrive.GoogleSignInComposeHelper;
public class SearchFragment2 extends UIFragment<FileMeta> {
public static final String EMPTY_ID = "\u00A0";
@@ -356,6 +359,18 @@ public class SearchFragment2 extends UIFragment<FileMeta> {
handler = new Handler(Looper.getMainLooper());
ComposeView composeView = (ComposeView) view.findViewById(R.id.compose_view);
GoogleSignInComposeHelper.createSimpleGoogleSignInButton(
composeView,
"Sign in with Google",
() -> {
// Handle sign-in click
LOG.d("Google Sign-In button clicked");
}
);
secondTopPanel = view.findViewById(R.id.secondTopPanel);
countBooks = (TextView) view.findViewById(R.id.countBooks);
onRefresh = view.findViewById(R.id.onRefresh);

View File

@@ -3,9 +3,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical">
<androidx.compose.ui.platform.ComposeView
android:layout_gravity="center_horizontal"
android:id="@+id/compose_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/filterLayout"
@@ -13,7 +19,7 @@
android:layout_height="wrap_content"
android:layout_margin="@dimen/panel_padding"
android:gravity="center_vertical"
android:orientation="horizontal" >
android:orientation="horizontal">
<ImageView
android:visibility="visible"
@@ -29,14 +35,13 @@
android:src="@drawable/glyphicons_600_menu" />
<RelativeLayout
android:layout_marginLeft="2dip"
android:layout_marginRight="2dip"
android:layout_width="match_parent"
android:layout_height="@dimen/wh_button"
android:layout_weight="1"
android:focusableInTouchMode="true" >
android:focusableInTouchMode="true">
<AutoCompleteTextView
android:id="@+id/filterLine"
@@ -107,7 +112,7 @@
android:layout_height="@dimen/wh_button"
android:background="@drawable/bg_search_second"
android:gravity="center_vertical"
android:orientation="horizontal" >
android:orientation="horizontal">
<TextView
android:contentDescription="@string/cd_sort_results"
@@ -167,8 +172,7 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
android:layout_height="match_parent">
<com.foobnix.ui2.fast.FastScrollRecyclerView
android:id="@+id/recyclerView"
@@ -179,8 +183,7 @@
app:handleColor="#999999"
app:hideScrollbar="true"
app:showTrack="false"
app:trackColor="#bbbbbb" >
</com.foobnix.ui2.fast.FastScrollRecyclerView>
app:trackColor="#bbbbbb"></com.foobnix.ui2.fast.FastScrollRecyclerView>
</FrameLayout>
</LinearLayout>

View File

@@ -50,6 +50,26 @@ android {
room {
schemaDirectory("$projectDir/schemas")
}
packaging {
resources {
excludes += setOf(
"META-INF/DEPENDENCIES",
"META-INF/NOTICE",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE.txt",
"META-INF/INDEX.LIST",
"META-INF/io.netty.versions.properties"
)
pickFirsts += setOf(
"google/protobuf/*.proto",
"META-INF/services/io.grpc.LoadBalancerProvider",
"META-INF/services/io.grpc.ManagedChannelProvider",
"META-INF/services/io.grpc.NameResolverProvider"
)
}
}
}
kotlin {
@@ -60,29 +80,10 @@ kotlin {
dependencies {
implementation(platform("com.google.firebase:firebase-bom:34.0.0"))
implementation("com.google.firebase:firebase-auth")
implementation("com.google.firebase:firebase-firestore")
//implementation("com.google.firebase:firebase-database") realtime
implementation("com.google.firebase:firebase-storage")
// Firebase BOM
implementation("androidx.credentials:credentials:1.5.0")
implementation("androidx.credentials:credentials-play-services-auth:1.5.0")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")
//google drive
implementation("com.google.android.gms:play-services-auth:21.4.0")
implementation("com.google.api-client:google-api-client-android:2.8.0") {
exclude(group = "org.apache.httpcomponents")
}
implementation("com.google.http-client:google-http-client-gson:1.47.1") {
exclude(group = "org.apache.httpcomponents")
}
implementation("com.google.apis:google-api-services-drive:v3-rev20230822-2.0.0") {
exclude(group = "org.apache.httpcomponents")
}
//end google drive
implementation(project(":googleDrive"))
//BOM begin
implementation(platform("androidx.compose:compose-bom:2025.07.00"))
@@ -94,25 +95,23 @@ dependencies {
implementation("androidx.compose.material3:material3")
//BOM end
implementation("androidx.datastore:datastore:1.1.7")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("androidx.activity:activity-compose:1.10.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.2")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.navigation:navigation-compose:2.9.3")
implementation("io.coil-kt.coil3:coil-compose:3.3.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0")
implementation("com.squareup.okhttp3:okhttp:5.1.0")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.navigation:navigation-compose:2.9.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("androidx.datastore:datastore:1.1.7")
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation(platform("io.insert-koin:koin-bom:4.1.0"))
@@ -137,13 +136,13 @@ dependencies {
implementation("net.java.dev.jna:jna:5.17.0@aar")
implementation("io.github.vinceglb:filekit-dialogs:0.10.0-beta04")
implementation("io.github.vinceglb:filekit-dialogs-compose:0.10.0-beta04")
implementation("io.github.vinceglb:filekit-dialogs:0.10.0")
implementation("io.github.vinceglb:filekit-dialogs-compose:0.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2025.07.00"))
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")

View File

@@ -19,3 +19,19 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# gRPC rules
-keep class io.grpc.** {*;}
-keep class com.google.protobuf.** {*;}
-keepclassmembers class com.google.protobuf.** {
public <methods>;
}
-dontwarn io.grpc.**
-dontwarn com.google.protobuf.**
-dontwarn javax.naming.**
# Firebase rules
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.firebase.**
-dontwarn com.google.android.gms.**

View File

@@ -14,8 +14,6 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />

View File

@@ -19,7 +19,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import kotlinx.coroutines.launch
import mobi.librera.appcompose.components.GoogleSignInButton
import mobi.librera.lib.gdrive.GoogleSignInButton
import org.koin.androidx.compose.koinViewModel

View File

@@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
@@ -25,6 +26,30 @@ android {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
packaging {
resources {
excludes += setOf(
"META-INF/DEPENDENCIES",
"META-INF/NOTICE",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/NOTICE.txt",
"META-INF/INDEX.LIST",
"META-INF/io.netty.versions.properties"
)
pickFirsts += setOf(
"google/protobuf/*.proto",
"META-INF/services/io.grpc.LoadBalancerProvider",
"META-INF/services/io.grpc.ManagedChannelProvider",
"META-INF/services/io.grpc.NameResolverProvider"
)
}
}
}
kotlin {
@@ -34,6 +59,37 @@ kotlin {
}
dependencies {
implementation(platform("androidx.compose:compose-bom:2025.07.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.material:material-icons-core")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.10.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.2")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.fragment:fragment-ktx:1.8.8")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("io.coil-kt.coil3:coil-compose:3.3.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0")
implementation("com.squareup.okhttp3:okhttp:5.1.0")
api(platform("com.google.firebase:firebase-bom:34.0.0"))
api("com.google.firebase:firebase-auth")
api("com.google.firebase:firebase-firestore")
api("com.google.firebase:firebase-storage")//optional to remove
//gRPC dependencies for Firestore
api("io.grpc:grpc-okhttp:1.74.0")
api("io.grpc:grpc-android:1.74.0")
api("io.grpc:grpc-protobuf-lite:1.74.0")
api("io.grpc:grpc-stub:1.74.0")
api("com.google.android.gms:play-services-auth:21.4.0")
api("com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0")
api("com.google.api-client:google-api-client-android:2.8.0")
@@ -44,4 +100,5 @@ dependencies {
api("androidx.credentials:credentials:1.5.0")
api("androidx.credentials:credentials-play-services-auth:1.5.0")
api("com.google.android.libraries.identity.googleid:googleid:1.1.1")
}

View File

@@ -0,0 +1,101 @@
package mobi.librera.lib.gdrive
import android.annotation.SuppressLint
import androidx.core.net.toUri
import com.google.firebase.Firebase
import com.google.firebase.auth.auth
import com.google.firebase.firestore.firestore
import com.google.firebase.storage.storage
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.File
import kotlin.coroutines.resumeWithException
val KEY_USERS = "users"
val KEY_BOOKS = "books"
data class BookState(
val bookPath: String,
val fileName: String,
val progress: Float,
)
object FirestoreBooksRepository {
@SuppressLint("StaticFieldLeak")
private val db = Firebase.firestore
private val collection = db.collection("book_state")
fun listenToNotes(onChange: (List<BookState>) -> Unit) {
Firebase.auth.currentUser?.let { user ->
db.collection(KEY_USERS).document(user.uid).collection(KEY_BOOKS)
.addSnapshotListener { snapshot, _ ->
if (snapshot != null && !snapshot.isEmpty) {
val books = snapshot.toObjects(BookState::class.java)
onChange(books)
} else {
onChange(emptyList())
}
}
}
}
suspend fun uploadImageToFirebase(
book: File, coverFile: File
): String = suspendCancellableCoroutine { continuation ->
val storageRef = Firebase.storage
val userUID = Firebase.auth.currentUser?.uid
val imageRef = storageRef.getReference("users/$userUID/${book.name}")
println("uploadImageToFirebase ${imageRef.path} $imageRef")
imageRef.putFile(coverFile.toUri()).addOnSuccessListener {
imageRef.downloadUrl.addOnSuccessListener { downloadUrl ->
continuation.resume(downloadUrl.toString(), onCancellation = { a, b, c -> {} })
//imaUrl(downloadUrl.toString())
println("uploadImageToFirebase onSuccess |$downloadUrl")
}
}.addOnFailureListener { e ->
println("uploadImageToFirebase onError ${e.message}")
continuation.resumeWithException(e)
}
}
fun syncBook(book: BookState) = runBlocking {
// if (book.bookPaths[App.DEVICE_ID].isNullOrEmpty()) {
// println("Sync Book skip")
// return@runBlocking
// }
// launch {
// if (book.imageUrl.isEmpty()) {
// val bookPath = book.bookPaths[App.DEVICE_ID]
// if (!bookPath.isNullOrEmpty()) {
// val coverFile = File(bookPath)
// println("uploadImageToFirebase coverFile ${coverFile.isFile}")
// if (coverFile.isFile) {
// println("uploadImageToFirebase 2")
//
// book.imageUrl = uploadImageToFirebase(coverFile)
// println("Sync image url $book.imageUrl")
//
//
// }
// }
// }
println("Sync Book $book")
Firebase.auth.currentUser?.let { user ->
db.collection(KEY_USERS).document(user.uid).collection(KEY_BOOKS)
.document(book.fileName).set(book).addOnSuccessListener { documentReference ->
println("Sync success")
}.addOnFailureListener { e ->
println("Sync fail ${e.printStackTrace()}")
}
}
}
}

View File

@@ -1,4 +0,0 @@
package mobi.librera.lib.gdrive
class GoogleDrive {
}

View File

@@ -1,4 +1,4 @@
package mobi.librera.appcompose.components
package mobi.librera.lib.gdrive
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
@@ -14,10 +14,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mobi.librera.appcompose.R
import mobi.librera.appcompose.ui.theme.LibreraTheme
import com.foobnix.googledrive.R
@Composable
fun GoogleSignInButton(
@@ -50,11 +48,4 @@ fun GoogleSignInButton(
}
}
@Preview
@Composable
fun Preview() {
LibreraTheme {
GoogleSignInButton("Hello", onClick = {})
}
}

View File

@@ -0,0 +1,22 @@
package mobi.librera.lib.gdrive
import androidx.compose.ui.platform.ComposeView
object GoogleSignInComposeHelper {
@JvmStatic
fun createSimpleGoogleSignInButton(
composeView: ComposeView,
buttonText: String,
onSignInClick: Runnable
) {
composeView.setContent {
GoogleSignInButton(
text = buttonText,
onClick = {
onSignInClick.run()
}
)
}
}
}

View File

@@ -0,0 +1,81 @@
package mobi.librera.lib.gdrive
import androidx.compose.foundation.layout.Arrangement
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.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.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import kotlinx.coroutines.launch
@Composable
fun GoogleSignInScreen(
clientId: String
) {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val viewModel: GoogleSingInViewModel = viewModel()
val singInState by viewModel.singInState.collectAsState()
LaunchedEffect(Unit) {
viewModel.checkSingInState()
}
when (val state = singInState) {
is SingInState.NotSignIn -> {
GoogleSignInButton(
"Sing in with Google", onClick = {
scope.launch {
viewModel.signInWithGoogle(context, clientId)
}
})
}
is SingInState.Success -> {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.Start
) {
AsyncImage(
model = state.user.photoUrl,
modifier = Modifier
.size(40.dp)
.clip(CircleShape),
contentDescription = state.user.name
)
Column(Modifier.padding(start = 12.dp)) {
Text(state.user.name)
Text(state.user.email)
}
}
GoogleSignInButton(
"Sing out", onClick = {
scope.launch {
viewModel.signOut(context)
}
})
}
is SingInState.Error -> {
Text("Error: ${state.message}")
}
}
}

View File

@@ -0,0 +1,160 @@
package mobi.librera.lib.gdrive
import android.content.Context
import androidx.credentials.ClearCredentialStateRequest
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.exceptions.GetCredentialException
import androidx.lifecycle.ViewModel
import coil3.Uri
import coil3.toCoilUri
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.auth
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// TODO: These imports should be injected or passed as parameters to avoid circular dependency
// import mobi.librera.appcompose.App
// import mobi.librera.appcompose.R
// import mobi.librera.appcompose.room.BookRepository
data class User(val name: String, val email: String, val photoUrl: Uri?)
fun FirebaseUser?.toUser(): User = User(
this?.displayName.orEmpty(), this?.email.orEmpty(), this?.photoUrl?.toCoilUri()
)
sealed class SingInState {
data object NotSignIn : SingInState()
data class Success(val user: User) : SingInState()
data class Error(val message: String) : SingInState()
}
// Simplified version without BookRepository dependency to avoid circular dependency
class GoogleSingInViewModel() : ViewModel() {
private val _state = MutableStateFlow<SingInState>(SingInState.NotSignIn)
val singInState: StateFlow<SingInState> = _state
private fun observeFirestoreAndSyncToRoom() {
// TODO: This functionality should be moved to the app module where BookRepository is available
// For now, we'll just skip the sync functionality
println("Sync: Skipped due to circular dependency")
}
init {
// Skip initialization of sync for now
// observeFirestoreAndSyncToRoom()
}
fun checkSingInState() {
val auth = Firebase.auth
val currentUser = auth.currentUser
if (currentUser == null) {
_state.value = SingInState.NotSignIn
} else {
_state.value = SingInState.Success(currentUser.toUser())
}
}
suspend fun signInWithGoogle(context: Context, clientId: String) {
val credentialManager = CredentialManager.create(context)
val googleIdOption: GetGoogleIdOption =
GetGoogleIdOption.Builder().setFilterByAuthorizedAccounts(false)
.setServerClientId(clientId).build()
val request: GetCredentialRequest =
GetCredentialRequest.Builder().addCredentialOption(googleIdOption).build()
coroutineScope {
try {
val result = credentialManager.getCredential(
request = request,
context = context,
)
handleSignInWithGoogleOption(result)
} catch (e: GetCredentialException) {
e.printStackTrace()
_state.value = SingInState.Error(e.message.orEmpty())
}
}
}
private fun handleSignInWithGoogleOption(result: GetCredentialResponse) {
when (val credential = result.credential) {
is CustomCredential -> {
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
try {
val googleIdTokenCredential =
GoogleIdTokenCredential.createFrom(credential.data)
firebaseAuthWithGoogle(googleIdTokenCredential.idToken)
} catch (e: GoogleIdTokenParsingException) {
e.printStackTrace()
_state.value = SingInState.Error(e.message.orEmpty())
}
} else {
_state.value = SingInState.Error("Unexpected type of credential")
}
}
else -> {
_state.value = SingInState.Error("Unexpected Error")
}
}
}
private fun firebaseAuthWithGoogle(idToken: String) {
val credential = GoogleAuthProvider.getCredential(idToken, null)
val auth = Firebase.auth
auth.signInWithCredential(credential).addOnCompleteListener { task ->
if (task.isSuccessful) {
val user = auth.currentUser
println("current user $user")
_state.value = SingInState.Success(user.toUser())
observeFirestoreAndSyncToRoom()
} else {
_state.value = SingInState.Error(task.exception?.message.orEmpty())
}
}
}
suspend fun signOut(context: Context) {
val auth = Firebase.auth
val credentialManager = CredentialManager.create(context)
val clearRequest = ClearCredentialStateRequest()
coroutineScope {
credentialManager.clearCredentialState(clearRequest)
}
auth.signOut()
_state.value = SingInState.NotSignIn
}
}