Removed Built-In translator (it was based Closed Source libraries) + Removed all Closed Source libraries + LineWithTranslation → ReaderLine + Room migration 3 → 4.
This commit is contained in:
parent
e0d04b2c7d
commit
1e42c258e7
66 changed files with 256 additions and 4376 deletions
|
|
@ -92,19 +92,19 @@ dependencies {
|
|||
implementation("androidx.activity:activity-compose:1.9.0")
|
||||
|
||||
// Compose BOM libraries
|
||||
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
|
||||
implementation("androidx.compose.foundation:foundation")
|
||||
implementation("androidx.compose.animation:animation")
|
||||
implementation("androidx.compose.animation:animation-android")
|
||||
implementation("androidx.compose.foundation:foundation-layout")
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.ui:ui-android")
|
||||
// Compose BOM was eliminated - it is recognized as Closed Source in AboutLibraries.
|
||||
implementation("androidx.compose.foundation:foundation:1.6.8")
|
||||
implementation("androidx.compose.animation:animation:1.6.8")
|
||||
implementation("androidx.compose.animation:animation-android:1.6.8")
|
||||
implementation("androidx.compose.foundation:foundation-layout:1.6.8")
|
||||
implementation("androidx.compose.ui:ui:1.6.8")
|
||||
implementation("androidx.compose.ui:ui-graphics:1.6.8")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.6.8")
|
||||
implementation("androidx.compose.ui:ui-android:1.6.8")
|
||||
implementation("androidx.compose.material3:material3:1.3.0-alpha06")
|
||||
implementation("androidx.compose.material3:material3-window-size-class")
|
||||
implementation("androidx.compose.material:material-icons-extended")
|
||||
implementation("androidx.compose.material:material")
|
||||
implementation("androidx.compose.material3:material3-window-size-class:1.2.1")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.6.8")
|
||||
implementation("androidx.compose.material:material:1.6.8")
|
||||
|
||||
// All dependencies
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
|
||||
|
|
@ -165,15 +165,4 @@ dependencies {
|
|||
// About open source libraries
|
||||
implementation("com.mikepenz:aboutlibraries-core:latest.release")
|
||||
implementation("com.mikepenz:aboutlibraries-compose-m3:latest.release")
|
||||
|
||||
// Translator
|
||||
implementation("com.google.mlkit:translate:latest.release") {
|
||||
exclude("com.google.android.gms", "play-services-base")
|
||||
exclude("com.google.android.datatransport", "transport-api")
|
||||
exclude("com.google.android.datatransport", "transport-backend-cct")
|
||||
exclude("com.google.android.datatransport", "transport-runtime")
|
||||
exclude("com.google.firebase", "firebase-encoders-json")
|
||||
exclude("com.google.firebase", "firebase-encoders")
|
||||
}
|
||||
implementation("com.google.mlkit:language-id:latest.release")
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "c8a3497d25808dec49816f26182b61d6",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "BookEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `description` TEXT, `textPath` TEXT NOT NULL, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT, `category` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "textPath",
|
||||
"columnName": "textPath",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filePath",
|
||||
"columnName": "filePath",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scrollIndex",
|
||||
"columnName": "scrollIndex",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scrollOffset",
|
||||
"columnName": "scrollOffset",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "progress",
|
||||
"columnName": "progress",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "image",
|
||||
"columnName": "image",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "category",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HistoryEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bookId",
|
||||
"columnName": "bookId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"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, 'c8a3497d25808dec49816f26182b61d6')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -57,11 +57,6 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Remove Firebase logging-->
|
||||
<service
|
||||
android:name="com.google.android.datatransport.runtime.scheduling.jobscheduling.JobInfoSchedulerService"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- Save app locales -->
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,6 @@ package ua.acclorite.book_story.data.di
|
|||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import com.google.mlkit.common.model.RemoteModelManager
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentification
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentificationOptions
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentifier
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dagger.Module
|
||||
|
|
@ -52,20 +48,4 @@ object AppModule {
|
|||
.build()
|
||||
.dao
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideModelManager(): RemoteModelManager {
|
||||
return RemoteModelManager.getInstance()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLanguageIdentifier(): LanguageIdentifier {
|
||||
return LanguageIdentification.getClient(
|
||||
LanguageIdentificationOptions.Builder()
|
||||
.setConfidenceThreshold(0.2f)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,6 @@ import ua.acclorite.book_story.data.mapper.book.BookMapper
|
|||
import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
|
||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
|
||||
import ua.acclorite.book_story.data.mapper.language_history.LanguageHistoryMapper
|
||||
import ua.acclorite.book_story.data.mapper.language_history.LanguageHistoryMapperImpl
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.data.parser.FileParserImpl
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
|
|
@ -49,12 +47,6 @@ abstract class RepositoryModule {
|
|||
historyMapperImpl: HistoryMapperImpl
|
||||
): HistoryMapper
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindLanguageHistoryMapper(
|
||||
languageHistoryMapperImpl: LanguageHistoryMapperImpl
|
||||
): LanguageHistoryMapper
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindFileParser(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package ua.acclorite.book_story.data.local.dto
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
|
|
@ -17,10 +16,5 @@ data class BookEntity(
|
|||
val scrollOffset: Int,
|
||||
val progress: Float,
|
||||
val image: String? = null,
|
||||
val category: Category,
|
||||
@ColumnInfo(defaultValue = "false") val enableTranslator: Boolean,
|
||||
@ColumnInfo(defaultValue = "auto") val translateFrom: String,
|
||||
@ColumnInfo(defaultValue = "en") val translateTo: String,
|
||||
@ColumnInfo(defaultValue = "true") val doubleClickTranslation: Boolean,
|
||||
@ColumnInfo(defaultValue = "false") val translateWhenOpen: Boolean
|
||||
val category: Category
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
package ua.acclorite.book_story.data.local.dto
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class LanguageHistoryEntity(
|
||||
@PrimaryKey(false) val languageCode: String,
|
||||
val order: Int
|
||||
)
|
||||
|
|
@ -6,10 +6,8 @@ import androidx.room.Insert
|
|||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
||||
import ua.acclorite.book_story.data.local.dto.LanguageHistoryEntity
|
||||
|
||||
@Dao
|
||||
interface BookDao {
|
||||
|
|
@ -59,11 +57,4 @@ interface BookDao {
|
|||
|
||||
@Delete
|
||||
suspend fun deleteHistory(history: List<HistoryEntity>)
|
||||
|
||||
|
||||
@Upsert
|
||||
suspend fun updateLanguageHistory(history: List<LanguageHistoryEntity>)
|
||||
|
||||
@Query("SELECT * FROM languagehistoryentity")
|
||||
suspend fun getLanguageHistory(): List<LanguageHistoryEntity>
|
||||
}
|
||||
|
|
@ -2,23 +2,25 @@ package ua.acclorite.book_story.data.local.room
|
|||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||
import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
||||
import ua.acclorite.book_story.data.local.dto.LanguageHistoryEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
BookEntity::class,
|
||||
HistoryEntity::class,
|
||||
LanguageHistoryEntity::class
|
||||
HistoryEntity::class
|
||||
],
|
||||
version = 3,
|
||||
version = 4,
|
||||
autoMigrations = [
|
||||
AutoMigration(1, 2),
|
||||
AutoMigration(2, 3),
|
||||
AutoMigration(3, 4, spec = DatabaseHelper.MIGRATION_3_4::class),
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
|
|
@ -26,6 +28,7 @@ abstract class BookDatabase : RoomDatabase() {
|
|||
abstract val dao: BookDao
|
||||
}
|
||||
|
||||
@Suppress("ClassName")
|
||||
object DatabaseHelper {
|
||||
|
||||
val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||
|
|
@ -39,4 +42,12 @@ object DatabaseHelper {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteColumn("BookEntity", "enableTranslator")
|
||||
@DeleteColumn("BookEntity", "translateFrom")
|
||||
@DeleteColumn("BookEntity", "translateTo")
|
||||
@DeleteColumn("BookEntity", "doubleClickTranslation")
|
||||
@DeleteColumn("BookEntity", "translateWhenOpen")
|
||||
@DeleteTable("LanguageHistoryEntity")
|
||||
class MIGRATION_3_4 : AutoMigrationSpec
|
||||
}
|
||||
|
|
@ -20,12 +20,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
|||
textPath = book.textPath,
|
||||
description = book.description,
|
||||
image = if (book.coverImage != null) book.coverImage.toString() else null,
|
||||
category = book.category,
|
||||
enableTranslator = book.enableTranslator,
|
||||
translateFrom = book.translateFrom,
|
||||
translateTo = book.translateTo,
|
||||
doubleClickTranslation = book.doubleClickTranslation,
|
||||
translateWhenOpen = book.translateWhenOpen
|
||||
category = book.category
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -45,11 +40,6 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
|||
lastOpened = null,
|
||||
category = bookEntity.category,
|
||||
coverImage = if (bookEntity.image != null) Uri.parse(bookEntity.image) else null,
|
||||
enableTranslator = bookEntity.enableTranslator,
|
||||
translateFrom = bookEntity.translateFrom,
|
||||
translateTo = bookEntity.translateTo,
|
||||
doubleClickTranslation = bookEntity.doubleClickTranslation,
|
||||
translateWhenOpen = bookEntity.translateWhenOpen
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package ua.acclorite.book_story.data.mapper.language_history
|
||||
|
||||
import ua.acclorite.book_story.data.local.dto.LanguageHistoryEntity
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
|
||||
interface LanguageHistoryMapper {
|
||||
suspend fun toLanguageHistoryEntity(languageHistory: LanguageHistory): LanguageHistoryEntity
|
||||
|
||||
suspend fun toLanguageHistory(languageHistoryEntity: LanguageHistoryEntity): LanguageHistory
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package ua.acclorite.book_story.data.mapper.language_history
|
||||
|
||||
import ua.acclorite.book_story.data.local.dto.LanguageHistoryEntity
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
import javax.inject.Inject
|
||||
|
||||
class LanguageHistoryMapperImpl @Inject constructor() : LanguageHistoryMapper {
|
||||
|
||||
override suspend fun toLanguageHistoryEntity(languageHistory: LanguageHistory): LanguageHistoryEntity {
|
||||
return LanguageHistoryEntity(
|
||||
languageCode = languageHistory.languageCode,
|
||||
order = languageHistory.order
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun toLanguageHistory(languageHistoryEntity: LanguageHistoryEntity): LanguageHistory {
|
||||
return LanguageHistory(
|
||||
languageCode = languageHistoryEntity.languageCode,
|
||||
order = languageHistoryEntity.order
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,12 +55,7 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
coverImage = null
|
||||
) to coverImage
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
|
|||
|
|
@ -65,12 +65,7 @@ class Fb2FileParser @Inject constructor() : FileParser {
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
coverImage = null
|
||||
) to null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
|
|||
|
|
@ -43,12 +43,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
coverImage = null
|
||||
) to null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
|
|||
|
|
@ -31,12 +31,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
coverImage = null
|
||||
) to null
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
|
|||
|
|
@ -8,13 +8,6 @@ import android.os.Environment
|
|||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.google.mlkit.common.model.DownloadConditions
|
||||
import com.google.mlkit.common.model.RemoteModelManager
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentifier
|
||||
import com.google.mlkit.nl.translate.TranslateLanguage
|
||||
import com.google.mlkit.nl.translate.TranslateRemoteModel
|
||||
import com.google.mlkit.nl.translate.Translation
|
||||
import com.google.mlkit.nl.translate.TranslatorOptions
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -23,7 +16,6 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.local.data_store.DataStore
|
||||
|
|
@ -31,32 +23,25 @@ import ua.acclorite.book_story.data.local.notification.UpdatesNotificationServic
|
|||
import ua.acclorite.book_story.data.local.room.BookDao
|
||||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||
import ua.acclorite.book_story.data.mapper.language_history.LanguageHistoryMapper
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.data.remote.GithubAPI
|
||||
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.History
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.DataStoreConstants
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Singleton
|
||||
|
|
@ -64,15 +49,12 @@ class BookRepositoryImpl @Inject constructor(
|
|||
private val application: Application,
|
||||
private val database: BookDao,
|
||||
private val dataStore: DataStore,
|
||||
private val modelManager: RemoteModelManager,
|
||||
private val languageIdentifier: LanguageIdentifier,
|
||||
|
||||
private val githubAPI: GithubAPI,
|
||||
private val updatesNotificationService: UpdatesNotificationService,
|
||||
|
||||
private val bookMapper: BookMapper,
|
||||
private val historyMapper: HistoryMapper,
|
||||
private val languageHistoryMapper: LanguageHistoryMapper,
|
||||
|
||||
private val fileParser: FileParser,
|
||||
private val textParser: TextParser
|
||||
|
|
@ -190,34 +172,11 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val enableTranslator = dataStore.getNullableData(DataStoreConstants.ENABLE_TRANSLATOR)
|
||||
?: false
|
||||
|
||||
val translateFrom = dataStore.getNullableData(DataStoreConstants.TRANSLATE_FROM)
|
||||
?: "auto"
|
||||
|
||||
val translateTo = dataStore.getNullableData(DataStoreConstants.TRANSLATE_TO) ?: if (
|
||||
Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }
|
||||
) {
|
||||
Locale.getDefault().language.take(2)
|
||||
} else {
|
||||
"en"
|
||||
}
|
||||
|
||||
val doubleClickTranslation = dataStore.getNullableData(
|
||||
DataStoreConstants.DOUBLE_CLICK_TRANSLATION
|
||||
) ?: true
|
||||
|
||||
|
||||
val updatedBook = book.copy(
|
||||
textPath = "$booksDir/$textUri",
|
||||
coverImage = if (coverUri.isNotBlank()) {
|
||||
Uri.fromFile(File("$coversDir/$coverUri"))
|
||||
} else null,
|
||||
enableTranslator = enableTranslator,
|
||||
translateFrom = translateFrom,
|
||||
translateTo = translateTo,
|
||||
doubleClickTranslation = doubleClickTranslation
|
||||
} else null
|
||||
)
|
||||
|
||||
val bookToInsert = bookMapper.toBookEntity(updatedBook)
|
||||
|
|
@ -708,174 +667,6 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isLanguageModelDownloaded(languageCode: String): Boolean {
|
||||
val model = TranslateRemoteModel.Builder(languageCode).build()
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
modelManager.isModelDownloaded(model)
|
||||
.addOnSuccessListener {
|
||||
continuation.resume(it)
|
||||
}
|
||||
.addOnFailureListener {
|
||||
continuation.resume(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun downloadLanguageModel(
|
||||
languageCode: String,
|
||||
onCompleted: () -> Unit,
|
||||
onFailure: (Exception) -> Unit
|
||||
) {
|
||||
val isLanguageDownloaded = isLanguageModelDownloaded(languageCode)
|
||||
if (isLanguageDownloaded) {
|
||||
onCompleted()
|
||||
return
|
||||
}
|
||||
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
Log.i("TRANSLATOR", "Downloading Model: Coroutine Canceled")
|
||||
onCompleted()
|
||||
}
|
||||
|
||||
val language = TranslateRemoteModel.Builder(languageCode).build()
|
||||
val conditions = DownloadConditions.Builder().build()
|
||||
|
||||
modelManager.download(language, conditions)
|
||||
.addOnSuccessListener {
|
||||
onCompleted()
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
.addOnFailureListener {
|
||||
onFailure(it)
|
||||
onCompleted()
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
.addOnCanceledListener {
|
||||
onCompleted()
|
||||
Log.i("TRANSLATOR", "Downloading Model: Canceled")
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteLanguageModel(languageCode: String): Boolean {
|
||||
val isLanguageDownloaded = isLanguageModelDownloaded(languageCode)
|
||||
if (!isLanguageDownloaded) {
|
||||
return true
|
||||
}
|
||||
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val language = TranslateRemoteModel.Builder(languageCode).build()
|
||||
|
||||
modelManager.deleteDownloadedModel(language)
|
||||
.addOnSuccessListener {
|
||||
continuation.resume(true)
|
||||
}
|
||||
.addOnFailureListener {
|
||||
continuation.resume(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun identifyLanguage(text: String): Resource<String> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
languageIdentifier.identifyLanguage(text)
|
||||
.addOnSuccessListener {
|
||||
val languageCode = it.trim().lowercase().take(2)
|
||||
var identified = true
|
||||
|
||||
if (languageCode == "ru") {
|
||||
identified = false
|
||||
}
|
||||
|
||||
if (TranslateLanguage.fromLanguageTag(languageCode) == null) {
|
||||
identified = false
|
||||
}
|
||||
|
||||
if (identified) {
|
||||
continuation.resume(
|
||||
Resource.Success(languageCode)
|
||||
)
|
||||
return@addOnSuccessListener
|
||||
}
|
||||
|
||||
continuation.resume(
|
||||
Resource.Error(UIText.StringResource(R.string.error_language_not_identified))
|
||||
)
|
||||
}
|
||||
.addOnFailureListener {
|
||||
continuation.resume(
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_query,
|
||||
it.message ?: ""
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun translateText(
|
||||
sourceLanguage: LanguageCode,
|
||||
targetLanguage: LanguageCode,
|
||||
text: String
|
||||
): Resource<String> {
|
||||
return suspendCoroutine { continuation ->
|
||||
val translatorOptions = TranslatorOptions.Builder()
|
||||
.setSourceLanguage(sourceLanguage)
|
||||
.setTargetLanguage(targetLanguage)
|
||||
.build()
|
||||
val translator = Translation.getClient(translatorOptions)
|
||||
|
||||
translator.translate(text)
|
||||
.addOnSuccessListener {
|
||||
translator.close()
|
||||
continuation.resume(
|
||||
Resource.Success(
|
||||
it.trim()
|
||||
)
|
||||
)
|
||||
}
|
||||
.addOnFailureListener {
|
||||
translator.close()
|
||||
continuation.resume(
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_query,
|
||||
it.message ?: ""
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
.addOnCanceledListener {
|
||||
translator.close()
|
||||
continuation.resume(
|
||||
Resource.Error(
|
||||
UIText.StringResource(
|
||||
R.string.error_something_went_wrong
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLanguageHistory(): List<LanguageHistory> {
|
||||
return database.getLanguageHistory().map {
|
||||
languageHistoryMapper.toLanguageHistory(it)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateLanguageHistory(history: List<LanguageHistory>) {
|
||||
database.updateLanguageHistory(
|
||||
history.map {
|
||||
languageHistoryMapper.toLanguageHistoryEntity(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,4 @@ data class Book(
|
|||
|
||||
val lastOpened: Long?,
|
||||
val category: Category,
|
||||
|
||||
val enableTranslator: Boolean,
|
||||
val translateFrom: String,
|
||||
val translateTo: String,
|
||||
val doubleClickTranslation: Boolean,
|
||||
val translateWhenOpen: Boolean
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
import ua.acclorite.book_story.domain.util.Selected
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
|
||||
@Immutable
|
||||
data class Language(
|
||||
val languageCode: String,
|
||||
val displayLanguage: String,
|
||||
val isDownloading: Boolean,
|
||||
val isCanceled: Boolean,
|
||||
val isDownloaded: Boolean,
|
||||
val isSelected: Selected,
|
||||
val historyOrder: Int?
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class SelectableLanguage(
|
||||
val languageCode: String,
|
||||
val displayLanguage: String,
|
||||
val isDownloading: Boolean,
|
||||
val isCanceled: Boolean,
|
||||
val isDownloaded: Boolean,
|
||||
val isError: Boolean,
|
||||
val errorMessage: UIText? = null,
|
||||
val isSelected: Selected,
|
||||
val canUnselect: Boolean,
|
||||
val occurrences: List<ID>?
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class LanguageHistory(
|
||||
val languageCode: String,
|
||||
val order: Int
|
||||
)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
|
||||
@Parcelize
|
||||
@Immutable
|
||||
data class LineWithTranslation(
|
||||
val originalLine: String,
|
||||
|
||||
val useTranslation: Boolean = false,
|
||||
val translatingFrom: String = "",
|
||||
val translatingTo: String = "",
|
||||
val translatedLine: String? = null,
|
||||
|
||||
val isTranslationLoading: Boolean = false,
|
||||
val isTranslationFailed: Boolean = false,
|
||||
val errorMessage: UIText? = null
|
||||
) : Parcelable
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ua.acclorite.book_story.domain.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Immutable
|
||||
data class ReaderLine(
|
||||
val line: String
|
||||
) : Parcelable
|
||||
|
|
@ -6,10 +6,8 @@ import kotlinx.coroutines.flow.Flow
|
|||
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.History
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
import ua.acclorite.book_story.domain.model.NullableBook
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import java.io.File
|
||||
|
|
@ -91,30 +89,5 @@ interface BookRepository {
|
|||
history: List<History>
|
||||
)
|
||||
|
||||
|
||||
suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo?
|
||||
|
||||
|
||||
suspend fun isLanguageModelDownloaded(languageCode: String): Boolean
|
||||
|
||||
suspend fun downloadLanguageModel(
|
||||
languageCode: String,
|
||||
onCompleted: () -> Unit,
|
||||
onFailure: (Exception) -> Unit
|
||||
)
|
||||
|
||||
suspend fun deleteLanguageModel(languageCode: String): Boolean
|
||||
|
||||
suspend fun identifyLanguage(text: String): Resource<String>
|
||||
|
||||
suspend fun translateText(
|
||||
sourceLanguage: LanguageCode,
|
||||
targetLanguage: LanguageCode,
|
||||
text: String
|
||||
): Resource<String>
|
||||
|
||||
|
||||
suspend fun getLanguageHistory(): List<LanguageHistory>
|
||||
|
||||
suspend fun updateLanguageHistory(history: List<LanguageHistory>)
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeleteLanguageModel @Inject constructor(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(languageCode: String): Boolean {
|
||||
return repository.deleteLanguageModel(
|
||||
languageCode = languageCode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DownloadLanguageModel @Inject constructor(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(
|
||||
languageCode: String,
|
||||
onCompleted: () -> Unit,
|
||||
onFailure: (Exception) -> Unit
|
||||
) {
|
||||
repository.downloadLanguageModel(
|
||||
languageCode = languageCode,
|
||||
onCompleted = onCompleted,
|
||||
onFailure = onFailure
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetLanguageHistory @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(): List<LanguageHistory> {
|
||||
return repository.getLanguageHistory()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||
import ua.acclorite.book_story.domain.model.ReaderLine
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetText @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(textPath: String): Map<ID, LineWithTranslation> {
|
||||
suspend fun execute(textPath: String): Map<ID, ReaderLine> {
|
||||
return repository.getBookText(textPath = textPath).withIndex().associate {
|
||||
it.index to LineWithTranslation(originalLine = it.value)
|
||||
it.index to ReaderLine(line = it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import javax.inject.Inject
|
||||
|
||||
class IdentifyLanguage @Inject constructor(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(text: String): Resource<LanguageCode> {
|
||||
return repository.identifyLanguage(text)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class IsLanguageModelDownloaded @Inject constructor(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(languageCode: String): Boolean {
|
||||
return repository.isLanguageModelDownloaded(languageCode)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import javax.inject.Inject
|
||||
|
||||
class TranslateText @Inject constructor(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(
|
||||
sourceLanguage: LanguageCode,
|
||||
targetLanguage: LanguageCode,
|
||||
text: String
|
||||
): Resource<String> {
|
||||
return repository.translateText(
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package ua.acclorite.book_story.domain.use_case
|
||||
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpdateLanguageHistory @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(history: List<LanguageHistory>) {
|
||||
repository.updateLanguageHistory(history)
|
||||
}
|
||||
}
|
||||
|
|
@ -291,11 +291,6 @@ object Constants {
|
|||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
lastOpened = null,
|
||||
category = Category.READING,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
category = Category.READING
|
||||
)
|
||||
}
|
||||
|
|
@ -23,8 +23,5 @@ object DataStoreConstants {
|
|||
val PARAGRAPH_INDENTATION = booleanPreferencesKey("paragraph_indentation")
|
||||
val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
|
||||
val SIDE_PADDING = intPreferencesKey("side_padding")
|
||||
val ENABLE_TRANSLATOR = booleanPreferencesKey("enable_translator")
|
||||
val TRANSLATE_FROM = stringPreferencesKey("translate_from")
|
||||
val TRANSLATE_TO = stringPreferencesKey("translate_to")
|
||||
val DOUBLE_CLICK_TRANSLATION = booleanPreferencesKey("double_click_translation")
|
||||
}
|
||||
|
|
@ -6,6 +6,5 @@ import ua.acclorite.book_story.presentation.data.Navigator
|
|||
typealias CoverImage = Bitmap
|
||||
typealias Selected = Boolean
|
||||
typealias ID = Int
|
||||
typealias LanguageCode = String
|
||||
typealias Route = String
|
||||
typealias OnNavigate = (Navigator.() -> Unit) -> Unit
|
||||
|
|
|
|||
|
|
@ -1,298 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.components.translator_language
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AutoAwesome
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.components.CustomBottomSheet
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.components.TranslatorLanguageDownloadDialog
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.components.TranslatorLanguageItem
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.components.TranslatorLanguageItemWithIcon
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.components.TranslatorLanguageTopBar
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageEvent
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageState
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageViewModel
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
import ua.acclorite.book_story.presentation.ui.Transitions
|
||||
|
||||
/**
|
||||
* Translator language bottom sheet.
|
||||
* Manages downloading, deleting and selecting language for translator.
|
||||
*
|
||||
* @param selectedLanguage Current selected language.
|
||||
* @param unselectedLanguage Current unselected language(second).
|
||||
* @param translateFromSelecting Whether user selects "Translate from" or "Translate to" language.
|
||||
* @param onDismiss Dismissing BottomSheet.
|
||||
* @param onSelect Gives two variables, first - Translate from, second - Translate to. If user selects "Translate from" and then selects current unselected language(in this case "Translate to"), then it also changes "Translate to" to previous "Translate from" value.
|
||||
*/
|
||||
@Composable
|
||||
fun TranslatorLanguageBottomSheet(
|
||||
selectedLanguage: String,
|
||||
unselectedLanguage: String,
|
||||
translateFromSelecting: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSelect: (String, String) -> Unit
|
||||
) {
|
||||
val translatorLanguageViewModel: TranslatorLanguageViewModel = hiltViewModel()
|
||||
|
||||
val state = translatorLanguageViewModel.state.collectAsState()
|
||||
val loading = remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
translatorLanguageViewModel.init(
|
||||
selectedLanguage = selectedLanguage,
|
||||
unselectedLanguage = unselectedLanguage,
|
||||
translateFromSelecting = translateFromSelecting,
|
||||
loaded = {
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
TranslatorLanguageBottomSheetContent(
|
||||
state = state,
|
||||
loading = loading,
|
||||
onSelect = { from, to ->
|
||||
val fromSelecting = state.value.translateFromSelecting
|
||||
val languageToChoose = if (fromSelecting) from else to
|
||||
|
||||
if (languageToChoose != "auto") {
|
||||
translatorLanguageViewModel.onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguageHistory(
|
||||
languageToChoose
|
||||
)
|
||||
)
|
||||
}
|
||||
onSelect(from, to)
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
onEvent = translatorLanguageViewModel::onEvent
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TranslatorLanguageBottomSheetContent(
|
||||
state: State<TranslatorLanguageState>,
|
||||
loading: State<Boolean>,
|
||||
onSelect: (String, String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onEvent: (TranslatorLanguageEvent) -> Unit
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
val languagesWithHistory = remember(state.value.languages) {
|
||||
state.value.languages.filter {
|
||||
it.historyOrder != null
|
||||
}.sortedBy {
|
||||
it.historyOrder
|
||||
}
|
||||
}
|
||||
val languages = remember(state.value.languages) {
|
||||
state.value.languages.filter {
|
||||
it.historyOrder == null
|
||||
}.sortedBy {
|
||||
it.languageCode
|
||||
}
|
||||
}
|
||||
|
||||
CustomBottomSheet(
|
||||
hasFixedHeight = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
dragHandle = {},
|
||||
shape = RoundedCornerShape(0),
|
||||
scrimColor = MaterialTheme.colorScheme.surface,
|
||||
onDismissRequest = {
|
||||
onDismiss()
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
containerColor = Color.Transparent,
|
||||
topBar = {
|
||||
TranslatorLanguageTopBar(
|
||||
state = state,
|
||||
scrollBehavior = scrollBehavior,
|
||||
onDismissBottomSheet = onDismiss,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
DefaultTransition(
|
||||
visible = !loading.value,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
if (state.value.showDownloadLanguageDialog) {
|
||||
TranslatorLanguageDownloadDialog(
|
||||
state = state,
|
||||
onSelect = onSelect,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
state.value.translateFromSelecting &&
|
||||
(!state.value.showSearch || state.value.searchQuery.isEmpty())
|
||||
) {
|
||||
item {
|
||||
val isSelected = remember(state.value.selectedLanguage) {
|
||||
state.value.selectedLanguage == "auto"
|
||||
}
|
||||
|
||||
TranslatorLanguageItemWithIcon(
|
||||
isSelected = isSelected,
|
||||
displayLanguage = stringResource(id = R.string.translator_auto),
|
||||
customIcon = Icons.Default.AutoAwesome,
|
||||
onSelect = {
|
||||
onSelect("auto", state.value.unselectedLanguage)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(36.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (languagesWithHistory.isNotEmpty()) {
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.translator_recent_languages),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
padding = 28.dp,
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(14.dp)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
customItems(
|
||||
languagesWithHistory,
|
||||
key = { it.languageCode }
|
||||
) { language ->
|
||||
TranslatorLanguageItem(
|
||||
language = language,
|
||||
onSelect = onSelect,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (languages.isNotEmpty()) {
|
||||
item {
|
||||
if (languagesWithHistory.isNotEmpty()) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(36.dp)
|
||||
)
|
||||
}
|
||||
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.translator_all_languages),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
padding = 28.dp,
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(14.dp)
|
||||
)
|
||||
}
|
||||
|
||||
customItems(
|
||||
languages,
|
||||
key = { it.languageCode }
|
||||
) { language ->
|
||||
TranslatorLanguageItem(
|
||||
language = language,
|
||||
onSelect = onSelect,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.height(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CustomAnimatedVisibility(
|
||||
visible = state.value.languages.isEmpty(),
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
exit = fadeOut(tween(0))
|
||||
) {
|
||||
IsEmpty(
|
||||
message = stringResource(id = R.string.translator_languages_empty),
|
||||
icon = painterResource(id = R.drawable.empty_languages)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.components.translator_language.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithContent
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageEvent
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageState
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Translator Language Download Dialog.
|
||||
* Used for downloading languages and selecting them right after.
|
||||
*
|
||||
* @param state [TranslatorLanguageState].
|
||||
* @param onSelect OnSelect callback. Happens when language is downloaded.
|
||||
* @param onEvent [TranslatorLanguageEvent] callback.
|
||||
*/
|
||||
@Composable
|
||||
fun TranslatorLanguageDownloadDialog(
|
||||
state: State<TranslatorLanguageState>,
|
||||
onSelect: (String, String) -> Unit,
|
||||
onEvent: (TranslatorLanguageEvent) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val displayLanguage = remember(state.value.languageToSelect) {
|
||||
val locale = Locale(state.value.languageToSelect)
|
||||
locale.getDisplayLanguage(locale).replaceFirstChar {
|
||||
it.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
CustomDialogWithContent(
|
||||
title = stringResource(id = R.string.download_language, displayLanguage),
|
||||
imageVectorIcon = Icons.Default.Download,
|
||||
description = stringResource(
|
||||
id = R.string.download_language_description
|
||||
),
|
||||
actionText = stringResource(id = R.string.download),
|
||||
isActionEnabled = true,
|
||||
disableOnClick = true,
|
||||
onDismiss = { onEvent(TranslatorLanguageEvent.OnDismissDownloadDialog) },
|
||||
onAction = {
|
||||
val selectedLanguage = state.value.languageToSelect
|
||||
|
||||
onEvent(TranslatorLanguageEvent.OnDismissDownloadDialog)
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnDownloadLanguage(
|
||||
languageCode = state.value.languageToSelect,
|
||||
onComplete = {
|
||||
if (selectedLanguage == state.value.languageToSelect) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.language_downloaded),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnSelectLanguage(
|
||||
languageCode = state.value.languageToSelect,
|
||||
onSelect = onSelect,
|
||||
onError = {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
it.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.error_query,
|
||||
it.message
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
withDivider = false
|
||||
)
|
||||
}
|
||||
|
|
@ -1,243 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.components.translator_language.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.Language
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageEvent
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
|
||||
/**
|
||||
* Translator Language Item with Icon.
|
||||
*
|
||||
* @param isSelected Whether item is selected.
|
||||
* @param displayLanguage Display language to be shown.
|
||||
* @param customIcon Icon.
|
||||
* @param onSelect OnSelect(click) callback.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.TranslatorLanguageItemWithIcon(
|
||||
isSelected: Boolean,
|
||||
displayLanguage: String,
|
||||
customIcon: ImageVector,
|
||||
onSelect: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(100))
|
||||
.background(
|
||||
if (isSelected) MaterialTheme.colorScheme.primaryContainer
|
||||
else Color.Transparent,
|
||||
RoundedCornerShape(100)
|
||||
)
|
||||
.clickable {
|
||||
onSelect()
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.selected_content_desc),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
displayLanguage,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Icon(
|
||||
imageVector = customIcon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translator Language Item with Icon.
|
||||
*
|
||||
* @param language [Language] item.
|
||||
* @param onSelect OnSelect(click) callback.
|
||||
* @param onEvent [TranslatorLanguageEvent] callback.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.TranslatorLanguageItem(
|
||||
language: Language,
|
||||
onSelect: (String, String) -> Unit,
|
||||
onEvent: (TranslatorLanguageEvent) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(100))
|
||||
.background(
|
||||
if (language.isSelected) MaterialTheme.colorScheme.primaryContainer
|
||||
else Color.Transparent,
|
||||
RoundedCornerShape(100)
|
||||
)
|
||||
.clickable(enabled = !language.isDownloading) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnSelectLanguage(
|
||||
languageCode = language.languageCode,
|
||||
onSelect = onSelect,
|
||||
onError = {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
it.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (language.isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.selected_content_desc),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
language.displayLanguage,
|
||||
color = if (language.isSelected) MaterialTheme.colorScheme.onPrimaryContainer
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Box(contentAlignment = Alignment.CenterEnd) {
|
||||
DefaultTransition(!language.isDownloaded && !language.isDownloading) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
contentDescription = stringResource(id = R.string.download),
|
||||
tint = if (language.isSelected) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null
|
||||
) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnDownloadLanguage(
|
||||
languageCode = language.languageCode,
|
||||
onComplete = {},
|
||||
onFailure = {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.error_query,
|
||||
it.message
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DefaultTransition(language.isDownloading && !language.isDownloaded) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.7.dp,
|
||||
modifier = Modifier
|
||||
.size(22.dp)
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null
|
||||
) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnCancelDownload(
|
||||
languageCode = language.languageCode
|
||||
)
|
||||
)
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(1.dp))
|
||||
}
|
||||
}
|
||||
|
||||
DefaultTransition(language.isDownloaded && !language.isDownloading) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(id = R.string.done),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null
|
||||
) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnDeleteLanguage(
|
||||
languageCode = language.languageCode
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package ua.acclorite.book_story.presentation.components.translator_language.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||
import ua.acclorite.book_story.presentation.components.CustomSearchTextField
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageEvent
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.data.TranslatorLanguageState
|
||||
|
||||
/**
|
||||
* Translator Language TopBar.
|
||||
*
|
||||
* @param state [TranslatorLanguageState].
|
||||
* @param scrollBehavior [TopAppBarScrollBehavior].
|
||||
* @param onDismissBottomSheet OnDismiss callback.
|
||||
* @param onEvent [TranslatorLanguageEvent] callback.
|
||||
*/
|
||||
@Composable
|
||||
fun TranslatorLanguageTopBar(
|
||||
state: State<TranslatorLanguageState>,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
onDismissBottomSheet: () -> Unit,
|
||||
onEvent: (TranslatorLanguageEvent) -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
AnimatedTopAppBar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
isTopBarScrolled = null,
|
||||
|
||||
content1Visibility = !state.value.showSearch,
|
||||
content1NavigationIcon = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Close,
|
||||
contentDescription = R.string.go_back_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onDismissBottomSheet()
|
||||
}
|
||||
},
|
||||
content1Title = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.translator_query, stringResource(
|
||||
if (state.value.translateFromSelecting) R.string.translator_from
|
||||
else R.string.translator_to
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
content1Actions = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Search,
|
||||
contentDescription = R.string.search_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(TranslatorLanguageEvent.OnSearchShowHide)
|
||||
}
|
||||
},
|
||||
|
||||
content2Visibility = state.value.showSearch,
|
||||
content2NavigationIcon = {
|
||||
CustomIconButton(
|
||||
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = R.string.exit_search_content_desc,
|
||||
disableOnClick = true
|
||||
) {
|
||||
onEvent(TranslatorLanguageEvent.OnSearchShowHide)
|
||||
}
|
||||
},
|
||||
content2Title = {
|
||||
CustomSearchTextField(
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
onEvent(TranslatorLanguageEvent.OnRequestFocus(focusRequester))
|
||||
},
|
||||
query = state.value.searchQuery,
|
||||
onQueryChange = {
|
||||
onEvent(TranslatorLanguageEvent.OnSearchQueryChange(it))
|
||||
},
|
||||
onSearch = { onEvent(TranslatorLanguageEvent.OnSearch) },
|
||||
placeholder = stringResource(
|
||||
id = R.string.search_query,
|
||||
stringResource(id = R.string.languages)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.components.translator_language.data
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import ua.acclorite.book_story.domain.model.Language
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
|
||||
@Immutable
|
||||
sealed class TranslatorLanguageEvent {
|
||||
data class OnSelectLanguage(
|
||||
val languageCode: String,
|
||||
val onError: suspend (UIText) -> Unit,
|
||||
val onSelect: (String, String) -> Unit
|
||||
) : TranslatorLanguageEvent()
|
||||
|
||||
data class OnDownloadLanguage(
|
||||
val languageCode: String,
|
||||
val onComplete: suspend () -> Unit,
|
||||
val onFailure: (Exception) -> Unit
|
||||
) : TranslatorLanguageEvent()
|
||||
|
||||
data class OnCancelDownload(
|
||||
val languageCode: String
|
||||
) : TranslatorLanguageEvent()
|
||||
|
||||
data class OnDeleteLanguage(
|
||||
val languageCode: String
|
||||
) : TranslatorLanguageEvent()
|
||||
|
||||
data object OnSearchShowHide : TranslatorLanguageEvent()
|
||||
data class OnRequestFocus(val focusRequester: FocusRequester) : TranslatorLanguageEvent()
|
||||
data class OnSearchQueryChange(val query: String) : TranslatorLanguageEvent()
|
||||
data object OnSearch : TranslatorLanguageEvent()
|
||||
data object OnDismissDownloadDialog : TranslatorLanguageEvent()
|
||||
data class OnUpdateLanguage(
|
||||
val languageCode: String,
|
||||
val block: (Language) -> Language
|
||||
) : TranslatorLanguageEvent()
|
||||
|
||||
data class OnUpdateLanguageHistory(
|
||||
val languageToChoose: LanguageCode
|
||||
) : TranslatorLanguageEvent()
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.components.translator_language.data
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.Language
|
||||
|
||||
@Immutable
|
||||
data class TranslatorLanguageState(
|
||||
val selectedLanguage: String = "",
|
||||
val unselectedLanguage: String = "",
|
||||
val languageToSelect: String = "",
|
||||
val translateFromSelecting: Boolean = false,
|
||||
val showDownloadLanguageDialog: Boolean = false,
|
||||
|
||||
val unfilteredLanguages: List<Language> = emptyList(),
|
||||
val languages: List<Language> = emptyList(),
|
||||
|
||||
val showSearch: Boolean = false,
|
||||
val searchQuery: String = "",
|
||||
val hasFocused: Boolean = false,
|
||||
|
||||
)
|
||||
|
|
@ -1,455 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.components.translator_language.data
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.mlkit.nl.translate.TranslateLanguage
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.Language
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
import ua.acclorite.book_story.domain.use_case.DeleteLanguageModel
|
||||
import ua.acclorite.book_story.domain.use_case.DownloadLanguageModel
|
||||
import ua.acclorite.book_story.domain.use_case.GetLanguageHistory
|
||||
import ua.acclorite.book_story.domain.use_case.IsLanguageModelDownloaded
|
||||
import ua.acclorite.book_story.domain.use_case.UpdateLanguageHistory
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TranslatorLanguageViewModel @Inject constructor(
|
||||
private val isLanguageModelDownloaded: IsLanguageModelDownloaded,
|
||||
private val downloadLanguageModel: DownloadLanguageModel,
|
||||
private val deleteLanguageModel: DeleteLanguageModel,
|
||||
private val getLanguageHistory: GetLanguageHistory,
|
||||
private val updateLanguageHistory: UpdateLanguageHistory,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(TranslatorLanguageState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
@Suppress("LABEL_NAME_CLASH")
|
||||
fun onEvent(event: TranslatorLanguageEvent) {
|
||||
when (event) {
|
||||
is TranslatorLanguageEvent.OnSelectLanguage -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (_state.value.showDownloadLanguageDialog) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
val isDownloaded = isLanguageModelDownloaded.execute(
|
||||
event.languageCode
|
||||
)
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
languageToSelect = event.languageCode
|
||||
)
|
||||
}
|
||||
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloaded = isDownloaded
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (isDownloaded) {
|
||||
val from = _state.value.translateFromSelecting
|
||||
val langToSelect = _state.value.languageToSelect
|
||||
val unselectedLang = _state.value.unselectedLanguage
|
||||
val selectedLang = _state.value.selectedLanguage
|
||||
|
||||
val translateFrom: String
|
||||
val translateTo: String
|
||||
|
||||
if (langToSelect == unselectedLang) {
|
||||
if (from) {
|
||||
if (selectedLang == "auto") {
|
||||
launch(Dispatchers.Main) {
|
||||
event.onError(
|
||||
UIText.StringResource(R.string.error_cannot_select_language)
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
translateFrom = langToSelect
|
||||
translateTo = selectedLang
|
||||
} else {
|
||||
translateTo = langToSelect
|
||||
translateFrom = selectedLang
|
||||
}
|
||||
} else {
|
||||
if (from) {
|
||||
translateFrom = langToSelect
|
||||
translateTo = unselectedLang
|
||||
} else {
|
||||
translateTo = langToSelect
|
||||
translateFrom = unselectedLang
|
||||
}
|
||||
}
|
||||
|
||||
if (translateTo == "auto") {
|
||||
launch(Dispatchers.Main) {
|
||||
event.onError(
|
||||
UIText.StringResource(R.string.error_wrong_language)
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
event.onSelect(
|
||||
translateFrom,
|
||||
translateTo
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDownloadLanguageDialog = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnDownloadLanguage -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloading = true,
|
||||
isCanceled = false
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val isDownloaded = isLanguageModelDownloaded.execute(
|
||||
event.languageCode
|
||||
)
|
||||
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloaded = isDownloaded
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (isDownloaded) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloading = false
|
||||
)
|
||||
}
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
downloadLanguageModel.execute(
|
||||
languageCode = event.languageCode,
|
||||
onCompleted = {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val modelDownloaded = isLanguageModelDownloaded.execute(
|
||||
event.languageCode
|
||||
)
|
||||
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloading = false,
|
||||
isDownloaded = modelDownloaded
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val isCanceled = _state.value.unfilteredLanguages.find {
|
||||
it.languageCode == event.languageCode
|
||||
}?.isCanceled
|
||||
|
||||
if (isCanceled == true && modelDownloaded) {
|
||||
deleteLanguageModel.execute(
|
||||
languageCode = event.languageCode
|
||||
)
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloading = false,
|
||||
isDownloaded = false,
|
||||
isCanceled = false
|
||||
)
|
||||
}
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
event.onComplete()
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
event.onFailure(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnCancelDownload -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloaded = false,
|
||||
isDownloading = false,
|
||||
isCanceled = true
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnDeleteLanguage -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val deleted = deleteLanguageModel.execute(event.languageCode)
|
||||
|
||||
if (deleted) {
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(event.languageCode) {
|
||||
it.copy(
|
||||
isDownloaded = false,
|
||||
isDownloading = false
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnSearchShowHide -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val shouldHide = _state.value.showSearch
|
||||
|
||||
if (shouldHide) {
|
||||
filterLanguages("")
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
searchQuery = "",
|
||||
hasFocused = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
showSearch = !shouldHide
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnSearchQueryChange -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
searchQuery = event.query
|
||||
)
|
||||
}
|
||||
job?.cancel()
|
||||
job = viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(500)
|
||||
yield()
|
||||
onEvent(TranslatorLanguageEvent.OnSearch)
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnSearch -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
filterLanguages()
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnRequestFocus -> {
|
||||
if (!_state.value.hasFocused) {
|
||||
event.focusRequester.requestFocus()
|
||||
_state.update {
|
||||
it.copy(
|
||||
hasFocused = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnDismissDownloadDialog -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDownloadLanguageDialog = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnUpdateLanguage -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val unfilteredLanguages = _state.value.unfilteredLanguages.map {
|
||||
if (it.languageCode == event.languageCode) {
|
||||
event.block(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
unfilteredLanguages = unfilteredLanguages
|
||||
)
|
||||
}
|
||||
filterLanguages()
|
||||
}
|
||||
}
|
||||
|
||||
is TranslatorLanguageEvent.OnUpdateLanguageHistory -> {
|
||||
viewModelScope.launch {
|
||||
var history = getLanguageHistory
|
||||
.execute()
|
||||
.sortedBy { it.order }
|
||||
.toMutableList()
|
||||
|
||||
val langToAdd = _state.value.languageToSelect
|
||||
|
||||
val firstItem = history.indexOfFirst { it.languageCode == langToAdd }
|
||||
val indexOfItem = if (firstItem != -1) firstItem else null
|
||||
|
||||
if (indexOfItem != null) {
|
||||
history.removeAt(indexOfItem)
|
||||
}
|
||||
history.add(0, LanguageHistory(langToAdd, 0))
|
||||
|
||||
history = history.mapIndexed { index, item ->
|
||||
item.copy(
|
||||
order = index
|
||||
)
|
||||
}.toMutableList()
|
||||
|
||||
updateLanguageHistory.execute(history)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun init(
|
||||
selectedLanguage: String,
|
||||
unselectedLanguage: String,
|
||||
translateFromSelecting: Boolean,
|
||||
loaded: () -> Unit
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val oldState = _state.value
|
||||
_state.update {
|
||||
TranslatorLanguageState()
|
||||
}
|
||||
|
||||
val history = getLanguageHistory.execute()
|
||||
val unfilteredLanguages = TranslateLanguage.getAllLanguages()
|
||||
.filter {
|
||||
if (it == "ru") {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
.map { languageCode ->
|
||||
val previousLanguage = oldState.languages.find {
|
||||
it.languageCode == languageCode
|
||||
}
|
||||
|
||||
val locale = Locale(languageCode)
|
||||
val displayLanguage = locale.getDisplayLanguage(locale).replaceFirstChar {
|
||||
it.uppercase()
|
||||
}
|
||||
|
||||
val isDownloading = previousLanguage?.isDownloading ?: false
|
||||
val isCancelled = previousLanguage?.isCanceled ?: false
|
||||
val isSelected = selectedLanguage == languageCode
|
||||
|
||||
val historyOrder = history.find {
|
||||
it.languageCode == languageCode
|
||||
}?.order
|
||||
|
||||
Language(
|
||||
languageCode = languageCode,
|
||||
displayLanguage = displayLanguage,
|
||||
isDownloading = isDownloading,
|
||||
isDownloaded = false,
|
||||
isCanceled = isCancelled,
|
||||
isSelected = isSelected,
|
||||
historyOrder = historyOrder
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedLanguage = selectedLanguage,
|
||||
unselectedLanguage = unselectedLanguage,
|
||||
translateFromSelecting = translateFromSelecting,
|
||||
unfilteredLanguages = unfilteredLanguages
|
||||
)
|
||||
}
|
||||
filterLanguages()
|
||||
loaded()
|
||||
loadDownloadedState()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadDownloadedState() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.value.unfilteredLanguages.map { language ->
|
||||
val isDownloaded = isLanguageModelDownloaded.execute(
|
||||
language.languageCode
|
||||
)
|
||||
val isDownloading = if (isDownloaded) false else null
|
||||
|
||||
onEvent(
|
||||
TranslatorLanguageEvent.OnUpdateLanguage(
|
||||
languageCode = language.languageCode,
|
||||
block = {
|
||||
it.copy(
|
||||
isDownloaded = isDownloaded,
|
||||
isDownloading = isDownloading ?: it.isDownloading
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterLanguages(
|
||||
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
||||
) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
languages = it.unfilteredLanguages.filter { language ->
|
||||
if (query.isBlank()) {
|
||||
return@filter true
|
||||
}
|
||||
|
||||
language.displayLanguage.lowercase().trim().contains(
|
||||
query.lowercase().trim()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,62 +9,3 @@ fun String.removeTrailingZero(): String {
|
|||
}
|
||||
|
||||
fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".")
|
||||
|
||||
fun <K, T> Map<K, T>.update(item: Pair<K, T>): Map<K, T> {
|
||||
val keyExists = containsKey(item.first)
|
||||
|
||||
return if (!keyExists) this
|
||||
else this.plus(item)
|
||||
}
|
||||
|
||||
fun <K, T> Map<K, T>.update(items: List<Pair<K, T>>): Map<K, T> {
|
||||
var map = this
|
||||
|
||||
for (item in items) {
|
||||
map = map.update(item)
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
fun <K, T> Map<K, T>.update(items: Map<K, T>): Map<K, T> {
|
||||
var map = this
|
||||
|
||||
for (item in items) {
|
||||
map = map.update(item.toPair())
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
fun <K, T> Map<K, T>.updateWithCopy(
|
||||
calculation: (T) -> T
|
||||
): Map<K, T> {
|
||||
var map = this
|
||||
|
||||
for (item in this) {
|
||||
map = map.update(item.key to calculation(item.value))
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
fun <K, T> Map<K, T>.removeKeys(
|
||||
keys: Set<K>
|
||||
): Map<K, T> {
|
||||
val map = this.toMutableMap()
|
||||
|
||||
for (key in keys) {
|
||||
map.remove(key)
|
||||
}
|
||||
|
||||
return map.toMap()
|
||||
}
|
||||
|
||||
fun <K, T> Map<K, T>.removeKey(
|
||||
key: K
|
||||
): Map<K, T> {
|
||||
val map = this.toMutableMap()
|
||||
map.remove(key)
|
||||
return map.toMap()
|
||||
}
|
||||
|
|
@ -17,8 +17,5 @@ sealed class MainEvent {
|
|||
data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeCheckForUpdates(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeSidePadding(val sidePadding: Int) : MainEvent()
|
||||
data class OnChangeEnableTranslator(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeTranslateFrom(val languageCode: String) : MainEvent()
|
||||
data class OnChangeTranslateTo(val languageCode: String) : MainEvent()
|
||||
data class OnChangeDoubleClickTranslation(val bool: Boolean) : MainEvent()
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import android.os.Parcelable
|
|||
import androidx.annotation.Keep
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.mlkit.nl.translate.TranslateLanguage
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.domain.util.DataStoreConstants
|
||||
|
|
@ -44,9 +43,6 @@ data class MainState(
|
|||
val showStartScreen: Boolean? = null,
|
||||
val checkForUpdates: Boolean? = null,
|
||||
val sidePadding: Int? = null,
|
||||
val enableTranslator: Boolean? = null,
|
||||
val translateFrom: String? = null,
|
||||
val translateTo: String? = null,
|
||||
val doubleClickTranslation: Boolean? = null
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
|
|
@ -109,23 +105,9 @@ data class MainState(
|
|||
val sidePadding: Int = data[SIDE_PADDING.name] as? Int
|
||||
?: 6
|
||||
|
||||
val enableTranslator: Boolean = data[ENABLE_TRANSLATOR.name] as? Boolean
|
||||
?: false
|
||||
|
||||
val translateFrom: String = data[TRANSLATE_FROM.name] as? String
|
||||
?: "auto"
|
||||
|
||||
val translateTo: String = data[TRANSLATE_TO.name] as? String ?: if (
|
||||
TranslateLanguage.getAllLanguages().any { it == language }
|
||||
) {
|
||||
language
|
||||
} else {
|
||||
"en"
|
||||
}
|
||||
|
||||
val doubleClickTranslation: Boolean =
|
||||
data[DOUBLE_CLICK_TRANSLATION.name] as? Boolean
|
||||
?: true
|
||||
?: false
|
||||
|
||||
return MainState(
|
||||
language = language,
|
||||
|
|
@ -144,9 +126,6 @@ data class MainState(
|
|||
paragraphIndentation = paragraphIndentation,
|
||||
checkForUpdates = checkForUpdates,
|
||||
sidePadding = sidePadding,
|
||||
enableTranslator = enableTranslator,
|
||||
translateFrom = translateFrom,
|
||||
translateTo = translateTo,
|
||||
doubleClickTranslation = doubleClickTranslation
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,39 +228,6 @@ class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeEnableTranslator -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.ENABLE_TRANSLATOR, event.bool)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
enableTranslator = event.bool
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeTranslateFrom -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.TRANSLATE_FROM, event.languageCode)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
translateFrom = event.languageCode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeTranslateTo -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.TRANSLATE_TO, event.languageCode)
|
||||
updateStateWithSavedHandle {
|
||||
it.copy(
|
||||
translateTo = event.languageCode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MainEvent.OnChangeDoubleClickTranslation -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
setDatastore.execute(DataStoreConstants.DOUBLE_CLICK_TRANSLATION, event.bool)
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Confirm update dialog item.
|
||||
*/
|
||||
@Composable
|
||||
fun BookInfoConfirmUpdateDialogItem(title: String) {
|
||||
Text(
|
||||
text = "• $title",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
|
@ -534,7 +534,7 @@ class BookInfoViewModel @Inject constructor(
|
|||
val updatedText = updatedBook.text
|
||||
val text = getText.execute(book.textPath)
|
||||
|
||||
if (updatedText != text.map { it.value.originalLine }) {
|
||||
if (updatedText != text.map { it.value.line }) {
|
||||
textUpdated = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
|
|
@ -55,7 +54,6 @@ import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
|||
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.TranslatorLanguageBottomSheet
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
|
|
@ -66,13 +64,11 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewMode
|
|||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderEndItem
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderTextParagraph
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.download_language_dialog.ReaderDownloadLanguageDialog
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.start_item.ReaderStartItem
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.text.ReaderTextParagraph
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.translator_bottom_sheet.ReaderTranslatorBottomSheet
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||
|
|
@ -163,7 +159,6 @@ private fun ReaderScreen(
|
|||
onHistoryEvent: (HistoryEvent) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current as ComponentActivity
|
||||
val density = LocalDensity.current
|
||||
|
||||
val nestedScrollConnection = remember(state) {
|
||||
object : NestedScrollConnection {
|
||||
|
|
@ -214,45 +209,6 @@ private fun ReaderScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (state.value.showTranslatorBottomSheet) {
|
||||
ReaderTranslatorBottomSheet(
|
||||
state = state,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
if (state.value.showLanguageBottomSheet) {
|
||||
TranslatorLanguageBottomSheet(
|
||||
selectedLanguage = if (state.value.languageBottomSheetTranslateFrom) {
|
||||
state.value.book.translateFrom
|
||||
} else state.value.book.translateTo,
|
||||
|
||||
unselectedLanguage = if (!state.value.languageBottomSheetTranslateFrom) {
|
||||
state.value.book.translateFrom
|
||||
} else state.value.book.translateTo,
|
||||
|
||||
translateFromSelecting = state.value.languageBottomSheetTranslateFrom,
|
||||
onSelect = { from, to ->
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeTranslatorSettings(
|
||||
translateFrom = from,
|
||||
translateTo = to
|
||||
)
|
||||
)
|
||||
onEvent(
|
||||
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
},
|
||||
onDismiss = {
|
||||
onEvent(
|
||||
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
if (state.value.showSettingsBottomSheet) {
|
||||
ReaderSettingsBottomSheet(
|
||||
mainState = mainState,
|
||||
|
|
@ -260,12 +216,6 @@ private fun ReaderScreen(
|
|||
onMainEvent = onMainEvent
|
||||
)
|
||||
}
|
||||
if (state.value.showDownloadLanguageDialog) {
|
||||
ReaderDownloadLanguageDialog(
|
||||
state = state,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
@ -379,11 +329,7 @@ private fun ReaderScreen(
|
|||
text, key = { key -> key.first }
|
||||
) { line ->
|
||||
ReaderTextParagraph(
|
||||
state = state,
|
||||
id = line.first,
|
||||
line = line.second,
|
||||
context = context,
|
||||
density = density,
|
||||
fontFamily = fontFamily,
|
||||
fontColor = fontColor,
|
||||
lineHeight = lineHeight,
|
||||
|
|
@ -391,8 +337,6 @@ private fun ReaderScreen(
|
|||
fontSize = mainState.value.fontSize!!.sp,
|
||||
sidePadding = sidePadding,
|
||||
paragraphIndentation = mainState.value.paragraphIndentation!!,
|
||||
toolbarHidden = toolbarHidden,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.LineBreak
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import ua.acclorite.book_story.domain.model.FontWithName
|
||||
import ua.acclorite.book_story.domain.model.ReaderLine
|
||||
|
||||
/**
|
||||
* Reader Text Paragraph item.
|
||||
*
|
||||
* @param line Current line.
|
||||
* @param fontFamily Line's font family.
|
||||
* @param fontColor Line's font color.
|
||||
* @param lineHeight Line's line height.
|
||||
* @param fontStyle Line's font style.
|
||||
* @param fontSize Line's font size.
|
||||
* @param sidePadding Line's side padding.
|
||||
* @param paragraphIndentation Whether Paragraph Indentation is enabled for this line.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.ReaderTextParagraph(
|
||||
line: ReaderLine,
|
||||
fontFamily: FontWithName,
|
||||
fontColor: Color,
|
||||
lineHeight: TextUnit,
|
||||
fontStyle: FontStyle,
|
||||
fontSize: TextUnit,
|
||||
sidePadding: Dp,
|
||||
paragraphIndentation: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = sidePadding
|
||||
),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
BasicText(
|
||||
text = buildAnnotatedString {
|
||||
if (paragraphIndentation) {
|
||||
append(" ")
|
||||
}
|
||||
|
||||
append(line.line)
|
||||
},
|
||||
style = TextStyle(
|
||||
fontFamily = fontFamily.font,
|
||||
fontStyle = fontStyle,
|
||||
fontSize = fontSize,
|
||||
lineHeight = lineHeight,
|
||||
color = fontColor,
|
||||
lineBreak = LineBreak.Paragraph
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,7 @@ import androidx.compose.foundation.layout.Column
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
|
|
@ -141,17 +139,6 @@ fun ReaderTopBar(
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Translate,
|
||||
contentDescription = R.string.translator_content_desc,
|
||||
disableOnClick = false,
|
||||
enabled = !state.value.lockMenu,
|
||||
color = if (state.value.book.enableTranslator) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else LocalContentColor.current
|
||||
) {
|
||||
onEvent(ReaderEvent.OnShowHideTranslatorBottomSheet())
|
||||
}
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Settings,
|
||||
contentDescription = R.string.open_reader_settings_content_desc,
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.components.download_language_dialog
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.customItems
|
||||
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||
|
||||
/**
|
||||
* Reader Download Language Dialog. Downloads multiple language models at the same time.
|
||||
*
|
||||
* @param state [ReaderState].
|
||||
* @param onEvent [ReaderEvent] callback.
|
||||
*/
|
||||
@Composable
|
||||
fun ReaderDownloadLanguageDialog(
|
||||
state: State<ReaderState>,
|
||||
onEvent: (ReaderEvent) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var isOnClickEnabled by remember { mutableStateOf(true) }
|
||||
val isDownloading by remember {
|
||||
derivedStateOf {
|
||||
state.value.languagesToTranslate.any { it.isDownloading }
|
||||
}
|
||||
}
|
||||
val isDownloaded by remember {
|
||||
derivedStateOf {
|
||||
state.value.languagesToTranslate.filter { it.isSelected }.none { !it.isDownloaded }
|
||||
}
|
||||
}
|
||||
val languages = remember(state.value.languagesToTranslate, isDownloading) {
|
||||
if (isDownloading) {
|
||||
state.value.languagesToTranslate.sortedByDescending { it.isDownloading }
|
||||
} else {
|
||||
state.value.languagesToTranslate
|
||||
}
|
||||
}
|
||||
|
||||
CustomDialogWithLazyColumn(
|
||||
title = stringResource(id = R.string.download_languages),
|
||||
imageVectorIcon = Icons.Default.Download,
|
||||
description = stringResource(
|
||||
id = R.string.download_languages_description
|
||||
),
|
||||
actionText = stringResource(
|
||||
if (!isDownloaded) R.string.download
|
||||
else R.string.continue_download
|
||||
),
|
||||
disableOnClick = false,
|
||||
isActionEnabled = isOnClickEnabled,
|
||||
onDismiss = { onEvent(ReaderEvent.OnDismissDownloadLanguageDialog) },
|
||||
onAction = {
|
||||
isOnClickEnabled = false
|
||||
onEvent(
|
||||
ReaderEvent.OnDownloadLanguages(
|
||||
onSuccess = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.languages_downloaded),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
onEvent(
|
||||
ReaderEvent.OnTranslateText(
|
||||
it.keys,
|
||||
error = { error ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
error.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
error = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
withDivider = true,
|
||||
items = {
|
||||
customItems(
|
||||
languages, key = { it.languageCode }
|
||||
) {
|
||||
ReaderDownloadLanguageDialogItem(
|
||||
language = it,
|
||||
isEnabled = !isDownloading,
|
||||
onClick = {
|
||||
onEvent(
|
||||
ReaderEvent.OnSelectLanguage(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.components.download_language_dialog
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DownloadDone
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.SelectableLanguage
|
||||
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
|
||||
/**
|
||||
* Reader Download Language Dialog Item.
|
||||
*
|
||||
* @param language [SelectableLanguage] item.
|
||||
* @param isEnabled Whether OnClick is enabled or not.
|
||||
* @param onClick OnClick callback.
|
||||
*/
|
||||
@Composable
|
||||
fun LazyItemScope.ReaderDownloadLanguageDialogItem(
|
||||
language: SelectableLanguage,
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
if (
|
||||
isEnabled || language.isDownloading || language.isDownloaded || language.isError
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.animateItem(
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
enabled = !language.isDownloading &&
|
||||
!language.isDownloaded &&
|
||||
language.canUnselect &&
|
||||
isEnabled || language.isError
|
||||
) {
|
||||
if (!language.isError) {
|
||||
onClick()
|
||||
} else if (language.errorMessage != null) {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
language.errorMessage.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = language.displayLanguage,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (!language.occurrences.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.translator_occurrences,
|
||||
language.occurrences.size
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Box(contentAlignment = Alignment.CenterEnd) {
|
||||
DefaultTransition(
|
||||
language.isDownloading && !language.isDownloaded && !language.isError
|
||||
) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = 2.5.dp,
|
||||
modifier = Modifier
|
||||
.size(22.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DefaultTransition(
|
||||
language.isDownloaded && !language.isDownloading && !language.isError
|
||||
) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.DownloadDone,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(6.dp)
|
||||
.size(17.dp),
|
||||
contentDescription = stringResource(id = R.string.downloaded_content_desc)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DefaultTransition(
|
||||
!language.isDownloaded && !language.isDownloading && isEnabled && !language.isError
|
||||
) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
CustomCheckbox(
|
||||
selected = language.isSelected,
|
||||
size = 20.dp,
|
||||
containerColor =
|
||||
if (language.canUnselect) MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DefaultTransition(
|
||||
!language.isDownloaded && !language.isDownloading && language.isError
|
||||
) {
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.size(27.dp),
|
||||
contentDescription = stringResource(id = R.string.error_content_desc)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(1.5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.components.text
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.foundation.text.selection.DisableSelection
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.LineBreak
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.FontWithName
|
||||
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Reader Text Paragraph item.
|
||||
*
|
||||
* @param state [ReaderState].
|
||||
* @param id Current paragraph [ID].
|
||||
* @param line Current line.
|
||||
* @param context Context.
|
||||
* @param density [Density] from LocalDensity.current.
|
||||
* @param fontFamily Line's font family.
|
||||
* @param fontColor Line's font color.
|
||||
* @param lineHeight Line's line height.
|
||||
* @param fontStyle Line's font style.
|
||||
* @param fontSize Line's font size.
|
||||
* @param sidePadding Line's side padding.
|
||||
* @param paragraphIndentation Whether Paragraph Indentation is enabled for this line.
|
||||
* @param toolbarHidden Whether selection toolbar is hidden.
|
||||
* @param onEvent [ReaderEvent] callback.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LazyItemScope.ReaderTextParagraph(
|
||||
state: State<ReaderState>,
|
||||
id: ID,
|
||||
line: LineWithTranslation,
|
||||
context: Context,
|
||||
density: Density,
|
||||
fontFamily: FontWithName,
|
||||
fontColor: Color,
|
||||
lineHeight: TextUnit,
|
||||
fontStyle: FontStyle,
|
||||
fontSize: TextUnit,
|
||||
sidePadding: Dp,
|
||||
paragraphIndentation: Boolean,
|
||||
toolbarHidden: Boolean,
|
||||
onEvent: (ReaderEvent) -> Unit
|
||||
) {
|
||||
val text = remember(
|
||||
line.useTranslation, line.translatedLine, line.originalLine
|
||||
) {
|
||||
if (line.useTranslation && line.translatedLine != null) line.translatedLine
|
||||
else line.originalLine
|
||||
}
|
||||
|
||||
val translated = remember(line.useTranslation, line.translatedLine) {
|
||||
line.useTranslation && line.translatedLine != null
|
||||
}
|
||||
val showTrailingContent = remember(
|
||||
line.isTranslationLoading, line.isTranslationFailed
|
||||
) {
|
||||
line.isTranslationLoading || line.isTranslationFailed
|
||||
}
|
||||
|
||||
val translatingFrom = remember(line.translatingFrom) {
|
||||
val locale = Locale(line.translatingFrom)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
val translatingTo = remember(line.translatingTo) {
|
||||
val locale = Locale(line.translatingTo)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = sidePadding
|
||||
),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
CustomAnimatedVisibility(
|
||||
modifier = Modifier.animateItem(
|
||||
placementSpec = tween(10),
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
),
|
||||
visible = translated,
|
||||
enter = fadeIn(tween(250)) + expandVertically(tween(250)),
|
||||
exit = fadeOut(tween(250)) + shrinkVertically(tween(250)),
|
||||
) {
|
||||
DisableSelection {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.translator_show_original,
|
||||
translatingFrom,
|
||||
translatingTo
|
||||
),
|
||||
color = fontColor,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontWeight = FontWeight.Light,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.clickable(interactionSource = null, indication = null) {
|
||||
onEvent(ReaderEvent.OnUndoTranslation(id))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BasicText(
|
||||
text = buildAnnotatedString {
|
||||
if (paragraphIndentation) {
|
||||
append(" ")
|
||||
}
|
||||
|
||||
append(text)
|
||||
|
||||
if (showTrailingContent) {
|
||||
append(" ")
|
||||
appendInlineContent("translator")
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (
|
||||
state.value.book.enableTranslator &&
|
||||
state.value.book.doubleClickTranslation &&
|
||||
toolbarHidden
|
||||
) {
|
||||
Modifier.combinedClickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
onDoubleClick = {
|
||||
if (line.isTranslationLoading) {
|
||||
return@combinedClickable
|
||||
}
|
||||
|
||||
if (translated) {
|
||||
onEvent(ReaderEvent.OnUndoTranslation(id))
|
||||
} else {
|
||||
onEvent(
|
||||
ReaderEvent.OnTranslateText(setOf(id)) {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
it.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onEvent(
|
||||
ReaderEvent.OnShowHideMenu(context = context as ComponentActivity)
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
inlineContent = readerTextTrailingContent(
|
||||
line = line,
|
||||
color = fontColor,
|
||||
textSize = fontSize,
|
||||
context = context,
|
||||
density = density
|
||||
),
|
||||
style = TextStyle(
|
||||
fontFamily = fontFamily.font,
|
||||
fontStyle = fontStyle,
|
||||
fontSize = fontSize,
|
||||
lineHeight = lineHeight,
|
||||
color = fontColor,
|
||||
lineBreak = LineBreak.Paragraph
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.components.text
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||
import ua.acclorite.book_story.presentation.ui.DefaultTransition
|
||||
|
||||
/**
|
||||
* [ReaderTextParagraph]'s trailing content.
|
||||
*
|
||||
* @param line [ReaderTextParagraph]'s line.
|
||||
* @param color Color.
|
||||
* @param textSize Font size.
|
||||
* @param context Context.
|
||||
* @param density [Density] from LocalDensity.current.
|
||||
*/
|
||||
@Composable
|
||||
fun readerTextTrailingContent(
|
||||
line: LineWithTranslation,
|
||||
color: Color,
|
||||
textSize: TextUnit,
|
||||
context: Context,
|
||||
density: Density
|
||||
): Map<String, InlineTextContent> {
|
||||
val sizeDp = remember(textSize, density) { with(density) { textSize.toDp() } }
|
||||
return remember(line.isTranslationLoading, line.isTranslationFailed, sizeDp, color, textSize) {
|
||||
mapOf(
|
||||
Pair(
|
||||
"translator",
|
||||
InlineTextContent(
|
||||
placeholder = Placeholder(
|
||||
width = textSize,
|
||||
height = textSize,
|
||||
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
DefaultTransition(line.isTranslationLoading) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = sizeDp / 12,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(0.7f),
|
||||
color = color
|
||||
)
|
||||
}
|
||||
|
||||
DefaultTransition(line.isTranslationFailed && !line.isTranslationLoading) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Error,
|
||||
contentDescription = stringResource(id = R.string.error_content_desc),
|
||||
tint = color,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null
|
||||
) {
|
||||
line.errorMessage?.let {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
it.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.components.translator_bottom_sheet
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CustomBottomSheet
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWithTitle
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Reader Translator Bottom Sheet. You can set up preferences for an individual book here and apply/cancel translation.
|
||||
*
|
||||
* @param state [ReaderState].
|
||||
* @param onEvent [ReaderEvent] callback.
|
||||
*/
|
||||
@Composable
|
||||
fun ReaderTranslatorBottomSheet(
|
||||
state: State<ReaderState>,
|
||||
onEvent: (ReaderEvent) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val book = remember(state.value) { state.value.book }
|
||||
|
||||
val translateFrom = remember(book.translateFrom) {
|
||||
if (book.translateFrom == "auto") {
|
||||
return@remember context.getString(R.string.translator_auto_short)
|
||||
}
|
||||
|
||||
val locale = Locale(book.translateFrom)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
val translateTo = remember(book.translateTo) {
|
||||
val locale = Locale(book.translateTo)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
val loading = remember { mutableStateOf(false) }
|
||||
val textTranslated = remember(state.value.isTranslating, state.value.text) {
|
||||
loading.value = true
|
||||
|
||||
val hasTranslatedText = state.value.text.any {
|
||||
it.value.useTranslation && it.value.translatedLine != null
|
||||
}
|
||||
val loaded = state.value.text.none {
|
||||
it.value.isTranslationLoading
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
|
||||
!state.value.isTranslating && hasTranslatedText && loaded
|
||||
}
|
||||
|
||||
CustomBottomSheet(
|
||||
onDismissRequest = {
|
||||
onEvent(ReaderEvent.OnShowHideTranslatorBottomSheet())
|
||||
}
|
||||
) {
|
||||
LazyColumn(Modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp)
|
||||
.clip(RoundedCornerShape(25.dp))
|
||||
.background(
|
||||
if (book.enableTranslator) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.outline,
|
||||
RoundedCornerShape(25.dp)
|
||||
)
|
||||
.clickable {
|
||||
if (book.enableTranslator) {
|
||||
onEvent(ReaderEvent.OnCancelTranslation)
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeTranslatorSettings(
|
||||
enableTranslator = !book.enableTranslator
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 15.dp, vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.translator_reader_settings),
|
||||
color = if (book.enableTranslator) MaterialTheme.colorScheme.onPrimary
|
||||
else MaterialTheme.colorScheme.surface,
|
||||
fontSize = 17.5.sp
|
||||
)
|
||||
Switch(
|
||||
checked = book.enableTranslator,
|
||||
onCheckedChange = null,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
ReaderTranslatorBottomSheetLanguageChooser(
|
||||
fromLanguage = translateFrom,
|
||||
toLanguage = translateTo,
|
||||
onFromClick = {
|
||||
onEvent(
|
||||
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||
show = true,
|
||||
translateFrom = true
|
||||
)
|
||||
)
|
||||
},
|
||||
onToClick = {
|
||||
onEvent(
|
||||
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||
show = true,
|
||||
translateFrom = false
|
||||
)
|
||||
)
|
||||
},
|
||||
enabled = !state.value.isTranslating,
|
||||
switchEnabled = book.translateFrom != "auto",
|
||||
onSwitchClick = {
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeTranslatorSettings(
|
||||
translateFrom = book.translateTo,
|
||||
translateTo = book.translateFrom
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = book.doubleClickTranslation,
|
||||
title = stringResource(id = R.string.translator_double_click_to_translate_option),
|
||||
description = stringResource(id = R.string.translator_double_click_to_translate_option_desc),
|
||||
verticalPadding = 6.dp,
|
||||
onClick = {
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeTranslatorSettings(
|
||||
doubleClickTranslation = !book.doubleClickTranslation
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = book.translateWhenOpen,
|
||||
title = stringResource(id = R.string.translator_translate_on_start_option),
|
||||
description = stringResource(id = R.string.translator_translate_on_start_option_desc),
|
||||
verticalPadding = 6.dp,
|
||||
onClick = {
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeTranslatorSettings(
|
||||
translateWhenOpen = !book.translateWhenOpen
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 18.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(1f),
|
||||
onClick = {
|
||||
if (loading.value) {
|
||||
return@Button
|
||||
}
|
||||
|
||||
if (!state.value.isTranslating) {
|
||||
if (!textTranslated) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.translator_starting),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
onEvent(
|
||||
ReaderEvent.OnTranslateText(
|
||||
keys = null, /* means translating the whole text */
|
||||
error = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.translator_text_canceling),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
onEvent(
|
||||
ReaderEvent.OnUndoTranslation(
|
||||
null /* means canceling the whole text translation */
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.translator_canceling),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
loading.value = true
|
||||
onEvent(
|
||||
ReaderEvent.OnCancelTranslation
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (state.value.isTranslating || textTranslated) {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
},
|
||||
contentColor = contentColorFor(
|
||||
if (state.value.isTranslating || textTranslated) {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (!state.value.isTranslating && !loading.value) {
|
||||
Text(
|
||||
text = if (textTranslated) stringResource(
|
||||
id = R.string.undo_translation
|
||||
) else stringResource(id = R.string.translate)
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
color = LocalContentColor.current,
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier.size(21.dp),
|
||||
strokeWidth = 2.3.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier.height(
|
||||
8.dp + it
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.components.translator_bottom_sheet
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.CompareArrows
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||
|
||||
/**
|
||||
* Reader Translator BottomSheet Language Chooser.
|
||||
*
|
||||
* @param fromLanguage Current translate from language.
|
||||
* @param toLanguage Current translate to language.
|
||||
* @param enabled Whether it is enabled.
|
||||
* @param switchEnabled Whether switch languages button is enabled.
|
||||
* @param horizontalPadding Horizontal item padding.
|
||||
* @param onFromClick OnFromClick callback.
|
||||
* @param onToClick OnToClick callback.
|
||||
* @param onSwitchClick OnSwitchClick callback.
|
||||
*/
|
||||
@Composable
|
||||
fun ReaderTranslatorBottomSheetLanguageChooser(
|
||||
fromLanguage: String,
|
||||
toLanguage: String,
|
||||
enabled: Boolean = true,
|
||||
switchEnabled: Boolean = true,
|
||||
horizontalPadding: Dp = 18.dp,
|
||||
onFromClick: () -> Unit,
|
||||
onToClick: () -> Unit,
|
||||
onSwitchClick: () -> Unit
|
||||
) {
|
||||
val switchLanguage = remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
var job: Job? = null
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = horizontalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CustomLanguageSwitchingAnimation(
|
||||
visible = switchLanguage.value,
|
||||
rightPlacement = false
|
||||
) {
|
||||
LanguageItem(text = fromLanguage, enabled = enabled) {
|
||||
onFromClick()
|
||||
}
|
||||
}
|
||||
|
||||
CustomLanguageSwitchingAnimation(
|
||||
visible = !switchLanguage.value,
|
||||
rightPlacement = false
|
||||
) {
|
||||
LanguageItem(text = fromLanguage, enabled = enabled) {
|
||||
onFromClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
CustomIconButton(
|
||||
icon = Icons.AutoMirrored.Filled.CompareArrows,
|
||||
contentDescription = R.string.translator_language_content_desc,
|
||||
disableOnClick = false,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp),
|
||||
enabled = enabled && switchEnabled,
|
||||
onClick = {
|
||||
job?.cancel()
|
||||
job = scope.launch {
|
||||
switchLanguage.value = !switchLanguage.value
|
||||
|
||||
delay(100)
|
||||
yield()
|
||||
|
||||
onSwitchClick()
|
||||
}
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
CustomLanguageSwitchingAnimation(
|
||||
visible = switchLanguage.value,
|
||||
rightPlacement = true
|
||||
) {
|
||||
LanguageItem(text = toLanguage, enabled = enabled) {
|
||||
onToClick()
|
||||
}
|
||||
}
|
||||
|
||||
CustomLanguageSwitchingAnimation(
|
||||
visible = !switchLanguage.value,
|
||||
rightPlacement = true
|
||||
) {
|
||||
LanguageItem(text = toLanguage, enabled = enabled) {
|
||||
onToClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomLanguageSwitchingAnimation(
|
||||
visible: Boolean,
|
||||
rightPlacement: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val exitRightAnimation = remember {
|
||||
fadeOut(tween(200)) +
|
||||
slideOutHorizontally(tween(250)) { -it / 2 } +
|
||||
scaleOut(tween(250), 0.4f)
|
||||
}
|
||||
val enterRightAnimation = remember {
|
||||
slideInHorizontally(tween(350)) { -it / 2 } + fadeIn(tween(200))
|
||||
}
|
||||
|
||||
val exitLeftAnimation = remember {
|
||||
fadeOut(tween(200)) +
|
||||
slideOutHorizontally(tween(250)) { it / 2 } +
|
||||
scaleOut(tween(250), 0.4f)
|
||||
}
|
||||
val enterLeftAnimation = remember {
|
||||
slideInHorizontally(tween(350)) { it / 2 } + fadeIn(tween(200))
|
||||
}
|
||||
|
||||
CustomAnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = if (rightPlacement) enterRightAnimation else enterLeftAnimation,
|
||||
exit = if (rightPlacement) exitRightAnimation else exitLeftAnimation
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LanguageItem(text: String, enabled: Boolean, onClick: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.shapes.large)
|
||||
.clickable(enabled = enabled) {
|
||||
onClick()
|
||||
}
|
||||
.padding(horizontal = 4.dp, vertical = 14.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,6 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||
import ua.acclorite.book_story.domain.model.SelectableLanguage
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.OnNavigate
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
|
||||
|
|
@ -25,44 +21,6 @@ sealed class ReaderEvent {
|
|||
val context: ComponentActivity
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnShowHideTranslatorBottomSheet(val show: Boolean? = null) : ReaderEvent()
|
||||
|
||||
data class OnChangeTranslatorSettings(
|
||||
val enableTranslator: Boolean? = null,
|
||||
val translateFrom: String? = null,
|
||||
val translateTo: String? = null,
|
||||
val doubleClickTranslation: Boolean? = null,
|
||||
val translateWhenOpen: Boolean? = null
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnShowHideLanguageBottomSheet(
|
||||
val show: Boolean? = null,
|
||||
val translateFrom: Boolean? = null
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnTranslateText(
|
||||
val keys: Set<ID>?,
|
||||
val error: suspend (UIText) -> Unit
|
||||
) : ReaderEvent()
|
||||
|
||||
data object OnDismissDownloadLanguageDialog : ReaderEvent()
|
||||
|
||||
data object OnCancelTranslation : ReaderEvent()
|
||||
|
||||
data class OnUndoTranslation(val id: ID?) : ReaderEvent()
|
||||
|
||||
data class OnDownloadLanguages(
|
||||
val onSuccess: suspend (Map<ID, LineWithTranslation>) -> Unit,
|
||||
val error: suspend (UIText) -> Unit
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnSelectLanguage(val language: SelectableLanguage) : ReaderEvent()
|
||||
|
||||
data class OnUpdateLanguage(
|
||||
val languageCode: LanguageCode,
|
||||
val calculation: (SelectableLanguage) -> SelectableLanguage
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnGoBack(
|
||||
val context: ComponentActivity,
|
||||
val navigate: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ package ua.acclorite.book_story.presentation.screens.reader.data
|
|||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||
import ua.acclorite.book_story.domain.model.SelectableLanguage
|
||||
import ua.acclorite.book_story.domain.model.ReaderLine
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
|
|
@ -12,25 +11,17 @@ import ua.acclorite.book_story.domain.util.UIText
|
|||
@Immutable
|
||||
data class ReaderState(
|
||||
val book: Book = Constants.EMPTY_BOOK,
|
||||
val text: Map<ID, LineWithTranslation> = emptyMap(),
|
||||
val text: Map<ID, ReaderLine> = emptyMap(),
|
||||
val words: Int = 0,
|
||||
val letters: Int = 0,
|
||||
val listState: LazyListState = LazyListState(),
|
||||
|
||||
val languagesToTranslate: List<SelectableLanguage> = emptyList(),
|
||||
val showDownloadLanguageDialog: Boolean = false,
|
||||
val isTranslating: Boolean = false,
|
||||
|
||||
val errorMessage: UIText? = null,
|
||||
val loading: Boolean = true,
|
||||
|
||||
val showMenu: Boolean = false,
|
||||
val lockMenu: Boolean = false,
|
||||
|
||||
val showTranslatorBottomSheet: Boolean = false,
|
||||
val showLanguageBottomSheet: Boolean = false,
|
||||
val languageBottomSheetTranslateFrom: Boolean = false,
|
||||
|
||||
val showSettingsBottomSheet: Boolean = false,
|
||||
val currentPage: Int = 0,
|
||||
)
|
||||
|
|
@ -13,10 +13,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -25,34 +22,18 @@ import kotlinx.coroutines.flow.debounce
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||
import ua.acclorite.book_story.domain.model.SelectableLanguage
|
||||
import ua.acclorite.book_story.domain.use_case.DeleteLanguageModel
|
||||
import ua.acclorite.book_story.domain.use_case.DownloadLanguageModel
|
||||
import ua.acclorite.book_story.domain.use_case.GetBookById
|
||||
import ua.acclorite.book_story.domain.use_case.GetLatestHistory
|
||||
import ua.acclorite.book_story.domain.use_case.GetText
|
||||
import ua.acclorite.book_story.domain.use_case.IdentifyLanguage
|
||||
import ua.acclorite.book_story.domain.use_case.IsLanguageModelDownloaded
|
||||
import ua.acclorite.book_story.domain.use_case.TranslateText
|
||||
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.OnNavigate
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import ua.acclorite.book_story.presentation.data.Screen
|
||||
import ua.acclorite.book_story.presentation.data.removeKey
|
||||
import ua.acclorite.book_story.presentation.data.removeKeys
|
||||
import ua.acclorite.book_story.presentation.data.update
|
||||
import ua.acclorite.book_story.presentation.data.updateWithCopy
|
||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
|
||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
|
@ -62,12 +43,7 @@ class ReaderViewModel @Inject constructor(
|
|||
private val updateBooks: UpdateBooks,
|
||||
private val getText: GetText,
|
||||
private val getLatestHistory: GetLatestHistory,
|
||||
private val getBookById: GetBookById,
|
||||
private val identifyLanguage: IdentifyLanguage,
|
||||
private val isLanguageModelDownloaded: IsLanguageModelDownloaded,
|
||||
private val downloadLanguageModel: DownloadLanguageModel,
|
||||
private val deleteLanguageModel: DeleteLanguageModel,
|
||||
private val translateText: TranslateText,
|
||||
private val getBookById: GetBookById
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(ReaderState())
|
||||
|
|
@ -75,12 +51,6 @@ class ReaderViewModel @Inject constructor(
|
|||
|
||||
private var eventJob = SupervisorJob()
|
||||
|
||||
private var dismissTranslatorJob: Job? = null
|
||||
private var cancelTranslationJob: Job? = null
|
||||
private var translatorJob: Job? = null
|
||||
private var undoTranslationJob: Job? = null
|
||||
private var downloadLanguageJob: Job? = null
|
||||
|
||||
fun onEvent(event: ReaderEvent) {
|
||||
viewModelScope.launch(eventJob + Dispatchers.Main) {
|
||||
when (event) {
|
||||
|
|
@ -102,7 +72,7 @@ class ReaderViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
val textAsLine = text
|
||||
.map { it.value.originalLine }
|
||||
.map { it.value.line }
|
||||
.joinToString(
|
||||
separator = "\n"
|
||||
)
|
||||
|
|
@ -181,17 +151,6 @@ class ReaderViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
if (
|
||||
_state.value.book.translateWhenOpen &&
|
||||
_state.value.book.enableTranslator
|
||||
) {
|
||||
onEvent(
|
||||
ReaderEvent.OnTranslateText(null) {
|
||||
event.onError(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return@collectLatest
|
||||
}
|
||||
}
|
||||
|
|
@ -229,653 +188,6 @@ class ReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnShowHideTranslatorBottomSheet -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showTranslatorBottomSheet = event.show
|
||||
?: !it.showTranslatorBottomSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnChangeTranslatorSettings -> {
|
||||
launch(Dispatchers.IO) {
|
||||
val book = _state.value.book
|
||||
val updatedBook = book.copy(
|
||||
enableTranslator = event.enableTranslator ?: book.enableTranslator,
|
||||
translateFrom = event.translateFrom ?: book.translateFrom,
|
||||
translateTo = event.translateTo ?: book.translateTo,
|
||||
doubleClickTranslation = event.doubleClickTranslation
|
||||
?: book.doubleClickTranslation,
|
||||
translateWhenOpen = event.translateWhenOpen
|
||||
?: book.translateWhenOpen
|
||||
)
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = updatedBook
|
||||
)
|
||||
}
|
||||
updateBooks.execute(listOf(updatedBook))
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnShowHideLanguageBottomSheet -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showLanguageBottomSheet = event.show ?: !it.showLanguageBottomSheet,
|
||||
languageBottomSheetTranslateFrom = event.translateFrom ?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnTranslateText -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isTranslating = true
|
||||
)
|
||||
}
|
||||
|
||||
launch {
|
||||
joinJobs()
|
||||
|
||||
translatorJob = launch(Dispatchers.IO) {
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeTranslatorSettings(
|
||||
enableTranslator = true
|
||||
)
|
||||
)
|
||||
|
||||
val keys = event.keys ?: _state.value.text.keys
|
||||
var linesToTranslate = if (keys.size >= _state.value.text.size) {
|
||||
_state.value.text
|
||||
} else {
|
||||
_state.value.text.filterKeys { keys.any { key -> key == it } }
|
||||
}
|
||||
|
||||
val languages = mutableMapOf<LanguageCode, List<ID>?>()
|
||||
|
||||
yield()
|
||||
linesToTranslate = linesToTranslate.updateWithCopy {
|
||||
it.copy(
|
||||
isTranslationLoading = true,
|
||||
isTranslationFailed = false,
|
||||
errorMessage = null,
|
||||
useTranslation = false
|
||||
)
|
||||
}
|
||||
|
||||
yield()
|
||||
onUpdateTextLines(
|
||||
linesToTranslate,
|
||||
wholeText = linesToTranslate.size >= _state.value.text.size
|
||||
)
|
||||
|
||||
yield()
|
||||
if (_state.value.book.translateFrom == "auto") {
|
||||
val identifiedLanguages =
|
||||
mutableListOf<Pair<LanguageCode, ID>>()
|
||||
val errors = mutableMapOf<ID, LineWithTranslation>()
|
||||
|
||||
for (line in linesToTranslate.toMap()) {
|
||||
yield()
|
||||
val result =
|
||||
identifyLanguage.execute(line.value.originalLine)
|
||||
|
||||
when (result) {
|
||||
is Resource.Success -> {
|
||||
identifiedLanguages.add(
|
||||
result.data!! to line.key
|
||||
)
|
||||
}
|
||||
|
||||
is Resource.Error -> {
|
||||
errors[line.key] = line.value.copy(
|
||||
isTranslationFailed = true,
|
||||
isTranslationLoading = false,
|
||||
errorMessage = result.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield()
|
||||
if (errors.isNotEmpty()) {
|
||||
onUpdateTextLines(
|
||||
errors,
|
||||
errors.size >= _state.value.text.size
|
||||
)
|
||||
linesToTranslate = linesToTranslate.removeKeys(errors.keys)
|
||||
}
|
||||
|
||||
yield()
|
||||
languages.putAll(
|
||||
identifiedLanguages
|
||||
.groupBy {
|
||||
it.first
|
||||
}.mapValues { (_, value) ->
|
||||
value.map { it.second }
|
||||
}
|
||||
)
|
||||
} else {
|
||||
languages[_state.value.book.translateFrom] = emptyList()
|
||||
}
|
||||
|
||||
yield()
|
||||
if (languages.isEmpty()) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isTranslating = false
|
||||
)
|
||||
}
|
||||
|
||||
yield()
|
||||
launch(Dispatchers.Main) {
|
||||
event.error(UIText.StringResource(R.string.error_languages_not_identified))
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
var shouldShowDialog = false
|
||||
|
||||
if (languages.any { !isLanguageModelDownloaded.execute(it.key) }) {
|
||||
shouldShowDialog = true
|
||||
}
|
||||
|
||||
if (!isLanguageModelDownloaded.execute(_state.value.book.translateTo)) {
|
||||
shouldShowDialog = true
|
||||
}
|
||||
|
||||
yield()
|
||||
if (shouldShowDialog) {
|
||||
val languagesToDownload = mutableListOf<SelectableLanguage>()
|
||||
|
||||
val translateToLocale = Locale(_state.value.book.translateTo)
|
||||
val translateToDisplayLanguage = translateToLocale
|
||||
.getDisplayLanguage(translateToLocale)
|
||||
.replaceFirstChar {
|
||||
it.uppercase()
|
||||
}
|
||||
|
||||
val translateToCanceled =
|
||||
_state.value.languagesToTranslate.find {
|
||||
it.languageCode == _state.value.book.translateTo
|
||||
}?.isCanceled ?: false
|
||||
|
||||
languagesToDownload.add(
|
||||
SelectableLanguage(
|
||||
languageCode = _state.value.book.translateTo,
|
||||
displayLanguage = translateToDisplayLanguage,
|
||||
isDownloading = false,
|
||||
isCanceled = translateToCanceled,
|
||||
isError = false,
|
||||
isDownloaded = isLanguageModelDownloaded.execute(_state.value.book.translateTo),
|
||||
isSelected = true,
|
||||
canUnselect = false,
|
||||
occurrences = languages[_state.value.book.translateTo]
|
||||
)
|
||||
)
|
||||
languagesToDownload.addAll(
|
||||
languages
|
||||
.filterNot { it.key == _state.value.book.translateTo }
|
||||
.toList()
|
||||
.sortedByDescending { it.second?.size }
|
||||
.mapIndexed { index, language ->
|
||||
val locale = Locale(language.first)
|
||||
val displayLanguage = locale
|
||||
.getDisplayLanguage(locale)
|
||||
.replaceFirstChar {
|
||||
it.uppercase()
|
||||
}
|
||||
|
||||
val isCanceled =
|
||||
_state.value.languagesToTranslate.find {
|
||||
it.languageCode == language.first
|
||||
}?.isCanceled ?: false
|
||||
|
||||
SelectableLanguage(
|
||||
languageCode = language.first,
|
||||
displayLanguage = displayLanguage,
|
||||
isDownloading = false,
|
||||
isCanceled = isCanceled,
|
||||
isDownloaded = isLanguageModelDownloaded.execute(
|
||||
language.first
|
||||
),
|
||||
isError = false,
|
||||
isSelected = index == 0,
|
||||
canUnselect = index > 0,
|
||||
occurrences = language.second
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
languagesToTranslate = languagesToDownload,
|
||||
showDownloadLanguageDialog = true
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
yield()
|
||||
for (i in 0..<linesToTranslate.size) {
|
||||
yield()
|
||||
|
||||
val closestLine = findClosestLineToTranslate(
|
||||
linesToTranslate
|
||||
) ?: continue
|
||||
val line = linesToTranslate[closestLine] ?: continue
|
||||
|
||||
yield()
|
||||
val sourceLanguage =
|
||||
if (_state.value.book.translateFrom == "auto") {
|
||||
when (
|
||||
val result =
|
||||
identifyLanguage.execute(line.originalLine)
|
||||
) {
|
||||
is Resource.Success -> result.data!!
|
||||
is Resource.Error -> continue
|
||||
}
|
||||
} else {
|
||||
_state.value.book.translateFrom
|
||||
}
|
||||
val targetLanguage = _state.value.book.translateTo
|
||||
|
||||
yield()
|
||||
if (
|
||||
line.translatingFrom == sourceLanguage &&
|
||||
line.translatingTo == targetLanguage &&
|
||||
line.translatedLine != null
|
||||
) {
|
||||
val translatedLine = line.copy(
|
||||
useTranslation = true,
|
||||
isTranslationFailed = false,
|
||||
isTranslationLoading = false,
|
||||
errorMessage = null
|
||||
)
|
||||
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
text = it.text.update(
|
||||
closestLine to translatedLine
|
||||
)
|
||||
)
|
||||
}
|
||||
linesToTranslate = linesToTranslate.removeKey(closestLine)
|
||||
continue
|
||||
}
|
||||
|
||||
var translation: String? = null
|
||||
var error: UIText? = null
|
||||
|
||||
yield()
|
||||
val result = translateText.execute(
|
||||
sourceLanguage = sourceLanguage,
|
||||
targetLanguage = targetLanguage,
|
||||
text = line.originalLine
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is Resource.Success -> {
|
||||
translation = result.data
|
||||
}
|
||||
|
||||
is Resource.Error -> {
|
||||
error = result.message
|
||||
}
|
||||
}
|
||||
|
||||
val translatedLine = line.copy(
|
||||
useTranslation = translation != null,
|
||||
translatedLine = translation,
|
||||
translatingFrom = sourceLanguage,
|
||||
translatingTo = targetLanguage,
|
||||
isTranslationFailed = translation == null,
|
||||
isTranslationLoading = false,
|
||||
errorMessage = error
|
||||
)
|
||||
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
text = it.text.update(
|
||||
closestLine to translatedLine
|
||||
)
|
||||
)
|
||||
}
|
||||
linesToTranslate = linesToTranslate.removeKey(closestLine)
|
||||
}
|
||||
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
isTranslating = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnDismissDownloadLanguageDialog -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDownloadLanguageDialog = false,
|
||||
isTranslating = false
|
||||
)
|
||||
}
|
||||
|
||||
launch {
|
||||
joinJobs()
|
||||
|
||||
dismissTranslatorJob = launch(Dispatchers.IO) {
|
||||
val languagesToDownload = _state.value.languagesToTranslate
|
||||
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
languagesToTranslate = it.languagesToTranslate.map { lang ->
|
||||
lang.copy(
|
||||
isCanceled = true,
|
||||
isDownloading = false,
|
||||
isError = false
|
||||
)
|
||||
},
|
||||
showDownloadLanguageDialog = false
|
||||
)
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
downloadLanguageJob?.cancel()
|
||||
val lines = getLinesFromLanguages(languagesToDownload)
|
||||
onUpdateTextLines(
|
||||
lines.updateWithCopy {
|
||||
it.copy(
|
||||
isTranslationLoading = false,
|
||||
useTranslation = false
|
||||
)
|
||||
},
|
||||
wholeText = lines.size >= _state.value.text.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnCancelTranslation -> {
|
||||
launch {
|
||||
cancelTranslationJob?.join()
|
||||
undoTranslationJob?.join()
|
||||
dismissTranslatorJob?.cancel()
|
||||
translatorJob?.cancel()
|
||||
|
||||
cancelTranslationJob = launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isTranslating = false,
|
||||
showDownloadLanguageDialog = false
|
||||
)
|
||||
}
|
||||
|
||||
yield()
|
||||
onUpdateTextLines(
|
||||
_state.value.text.updateWithCopy {
|
||||
if (it.useTranslation) {
|
||||
return@updateWithCopy it
|
||||
}
|
||||
|
||||
it.copy(
|
||||
isTranslationLoading = false,
|
||||
useTranslation = false
|
||||
)
|
||||
},
|
||||
wholeText = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnUndoTranslation -> {
|
||||
launch {
|
||||
cancelTranslationJob?.join()
|
||||
undoTranslationJob?.join()
|
||||
|
||||
cancelTranslationJob = launch(Dispatchers.IO) {
|
||||
yield()
|
||||
|
||||
if (event.id != null) {
|
||||
_state.update {
|
||||
val line = it.text[event.id]?.copy(
|
||||
useTranslation = false,
|
||||
isTranslationLoading = false
|
||||
)
|
||||
|
||||
if (line == null) {
|
||||
return@update it
|
||||
}
|
||||
|
||||
it.copy(
|
||||
text = it.text.update(event.id to line)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
text = it.text.updateWithCopy { line ->
|
||||
if (!line.useTranslation) {
|
||||
return@updateWithCopy line
|
||||
}
|
||||
|
||||
line.copy(
|
||||
isTranslationLoading = false,
|
||||
useTranslation = false
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnDownloadLanguages -> {
|
||||
launch {
|
||||
downloadLanguageJob?.cancel()
|
||||
|
||||
downloadLanguageJob = launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
languagesToTranslate = it.languagesToTranslate.map { lang ->
|
||||
val isDownloaded = isLanguageModelDownloaded.execute(
|
||||
lang.languageCode
|
||||
)
|
||||
|
||||
lang.copy(
|
||||
isDownloading = !isDownloaded && lang.isSelected,
|
||||
isCanceled = false,
|
||||
isDownloaded = isDownloaded
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val shouldDownload = _state.value.languagesToTranslate.filter {
|
||||
it.isSelected
|
||||
}.any {
|
||||
it.isDownloading
|
||||
}
|
||||
|
||||
if (!shouldDownload) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
languagesToTranslate = it.languagesToTranslate.filter { lang ->
|
||||
lang.isDownloaded
|
||||
},
|
||||
showDownloadLanguageDialog = false
|
||||
)
|
||||
}
|
||||
|
||||
onUnselectLines()
|
||||
launch(Dispatchers.Main) {
|
||||
event.onSuccess(getLinesFromLanguages(_state.value.languagesToTranslate))
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val languagesToDownload = _state.value.languagesToTranslate.filter {
|
||||
it.isSelected
|
||||
}
|
||||
val jobs = languagesToDownload.map { lang ->
|
||||
async {
|
||||
downloadLanguageModel.execute(
|
||||
languageCode = lang.languageCode,
|
||||
onCompleted = {
|
||||
launch(Dispatchers.IO) {
|
||||
yield()
|
||||
|
||||
val modelDownloaded =
|
||||
isLanguageModelDownloaded.execute(lang.languageCode)
|
||||
if (!modelDownloaded) {
|
||||
onEvent(
|
||||
ReaderEvent.OnUpdateLanguage(lang.languageCode) {
|
||||
it.copy(
|
||||
isError = true,
|
||||
isDownloading = false,
|
||||
isDownloaded = false,
|
||||
errorMessage = UIText.StringResource(
|
||||
R.string.error_could_not_download_language
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ReaderEvent.OnUpdateLanguage(lang.languageCode) {
|
||||
it.copy(
|
||||
isDownloading = false,
|
||||
isDownloaded = true
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val isCanceled =
|
||||
_state.value.languagesToTranslate.find {
|
||||
it.languageCode == lang.languageCode
|
||||
}?.isCanceled ?: true
|
||||
|
||||
if (isCanceled) {
|
||||
deleteLanguageModel.execute(
|
||||
languageCode = lang.languageCode
|
||||
)
|
||||
onEvent(
|
||||
ReaderEvent.OnUpdateLanguage(lang.languageCode) {
|
||||
it.copy(
|
||||
isDownloading = false,
|
||||
isDownloaded = false,
|
||||
isCanceled = false
|
||||
)
|
||||
}
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
onEvent(
|
||||
ReaderEvent.OnUpdateLanguage(lang.languageCode) {
|
||||
it.copy(
|
||||
isError = true,
|
||||
isDownloading = false,
|
||||
isDownloaded = false,
|
||||
errorMessage = UIText.StringResource(
|
||||
R.string.error_query,
|
||||
error.message ?: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
jobs.awaitAll()
|
||||
|
||||
yield()
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
languagesToTranslate = it.languagesToTranslate.filter { lang ->
|
||||
isLanguageModelDownloaded.execute(lang.languageCode)
|
||||
}.map { lang -> lang.copy(isDownloaded = true) },
|
||||
showDownloadLanguageDialog = false
|
||||
)
|
||||
}
|
||||
|
||||
if (languagesToDownload.isEmpty()) {
|
||||
yield()
|
||||
launch(Dispatchers.Main) {
|
||||
event.error(
|
||||
UIText.StringResource(
|
||||
R.string.error_could_not_download_languages
|
||||
)
|
||||
)
|
||||
}
|
||||
onEvent(ReaderEvent.OnDismissDownloadLanguageDialog)
|
||||
return@launch
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
onUnselectLines()
|
||||
launch(Dispatchers.Main) {
|
||||
event.onSuccess(getLinesFromLanguages(_state.value.languagesToTranslate))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnSelectLanguage -> {
|
||||
launch(Dispatchers.IO) {
|
||||
val editedList = _state.value.languagesToTranslate.map {
|
||||
if (it.languageCode == event.language.languageCode) {
|
||||
it.copy(
|
||||
isSelected = !it.isSelected
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
languagesToTranslate = editedList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnUpdateLanguage -> {
|
||||
launch(Dispatchers.IO) {
|
||||
val editedList = _state.value.languagesToTranslate.map {
|
||||
if (it.languageCode == event.languageCode) {
|
||||
event.calculation(it)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
languagesToTranslate = editedList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ReaderEvent.OnGoBack -> {
|
||||
launch {
|
||||
_state.update {
|
||||
|
|
@ -1061,174 +373,6 @@ class ReaderViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun joinJobs() {
|
||||
cancelTranslationJob?.join()
|
||||
translatorJob?.join()
|
||||
dismissTranslatorJob?.join()
|
||||
undoTranslationJob?.join()
|
||||
}
|
||||
|
||||
private suspend fun findClosestLineToTranslate(
|
||||
translatingLines: Map<ID, LineWithTranslation>
|
||||
): ID? {
|
||||
val lines = translatingLines.toMutableMap()
|
||||
val visibleItems = _state.value.listState.layoutInfo.visibleItemsInfo
|
||||
val listStateIndex = (visibleItems.getOrNull(visibleItems.size / 2)?.index ?: 0) - 1
|
||||
.coerceAtLeast(0)
|
||||
|
||||
var closestLineIndex: Int? = null
|
||||
var searchUpperLine = false
|
||||
var currentLineIndex = listStateIndex
|
||||
val invalidIndices = mutableListOf<Int>()
|
||||
|
||||
while (true) {
|
||||
yield()
|
||||
|
||||
if (lines.isEmpty()) {
|
||||
break
|
||||
}
|
||||
|
||||
currentLineIndex = currentLineIndex.coerceIn(0..lines.maxOf { it.key })
|
||||
|
||||
if (lines[currentLineIndex] == null) {
|
||||
invalidIndices.add(currentLineIndex)
|
||||
|
||||
if (searchUpperLine) {
|
||||
val invalid = invalidIndices.filterNot {
|
||||
it >= currentLineIndex
|
||||
}
|
||||
var minus = 1
|
||||
|
||||
while (true) {
|
||||
if (invalid.any { currentLineIndex - minus == it }) {
|
||||
minus++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
currentLineIndex -= minus
|
||||
} else {
|
||||
val invalid = invalidIndices.filterNot {
|
||||
it <= currentLineIndex
|
||||
}
|
||||
var plus = 1
|
||||
|
||||
while (true) {
|
||||
if (invalid.any { currentLineIndex + plus == it }) {
|
||||
plus++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
currentLineIndex += plus
|
||||
}
|
||||
searchUpperLine = !searchUpperLine
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
lines[currentLineIndex]?.useTranslation == true ||
|
||||
lines[currentLineIndex]?.isTranslationFailed == true
|
||||
) {
|
||||
invalidIndices.add(currentLineIndex)
|
||||
lines.remove(currentLineIndex)
|
||||
|
||||
if (searchUpperLine) {
|
||||
val invalid = invalidIndices.filterNot {
|
||||
it >= currentLineIndex
|
||||
}
|
||||
var minus = 1
|
||||
|
||||
while (true) {
|
||||
if (invalid.any { currentLineIndex - minus == it }) {
|
||||
minus++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
currentLineIndex -= minus
|
||||
} else {
|
||||
val invalid = invalidIndices.filterNot {
|
||||
it <= currentLineIndex
|
||||
}
|
||||
var plus = 1
|
||||
|
||||
while (true) {
|
||||
if (invalid.any { currentLineIndex + plus == it }) {
|
||||
plus++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
currentLineIndex += plus
|
||||
}
|
||||
searchUpperLine = !searchUpperLine
|
||||
continue
|
||||
}
|
||||
|
||||
closestLineIndex = currentLineIndex
|
||||
break
|
||||
}
|
||||
|
||||
yield()
|
||||
return closestLineIndex
|
||||
}
|
||||
|
||||
private suspend fun onUpdateTextLines(
|
||||
newLines: Map<ID, LineWithTranslation>,
|
||||
wholeText: Boolean
|
||||
) {
|
||||
val newText = if (wholeText) newLines
|
||||
else _state.value.text.update(newLines)
|
||||
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
text = newText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onUnselectLines() {
|
||||
onUpdateTextLines(
|
||||
_state.value.text.updateWithCopy {
|
||||
it.copy(isTranslationLoading = false)
|
||||
},
|
||||
wholeText = true
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getLinesFromLanguages(
|
||||
languages: List<SelectableLanguage>
|
||||
): Map<ID, LineWithTranslation> {
|
||||
val lines = mutableMapOf<ID, LineWithTranslation>()
|
||||
|
||||
yield()
|
||||
for (language in languages) {
|
||||
yield()
|
||||
|
||||
if (language.occurrences == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (language.occurrences.isEmpty()) {
|
||||
lines += _state.value.text
|
||||
break
|
||||
}
|
||||
|
||||
lines += language.occurrences.associateWith {
|
||||
_state.value.text[it]!!
|
||||
}
|
||||
}
|
||||
yield()
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
fun init(
|
||||
screen: Screen.Reader,
|
||||
onNavigate: OnNavigate,
|
||||
|
|
|
|||
|
|
@ -15,9 +15,4 @@ sealed class SettingsEvent {
|
|||
val notificationsPermissionState: PermissionState,
|
||||
val onChangeCheckForUpdates: (Boolean) -> Unit
|
||||
) : SettingsEvent()
|
||||
|
||||
data class OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
val show: Boolean? = null,
|
||||
val translateFrom: Boolean? = null
|
||||
) : SettingsEvent()
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package ua.acclorite.book_story.presentation.screens.settings.data
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class SettingsState(
|
||||
val showReaderTranslatorLanguageBottomSheet: Boolean = false,
|
||||
val readerTranslatorLanguageBottomSheetTranslateFrom: Boolean = false,
|
||||
)
|
||||
|
|
@ -11,9 +11,6 @@ import com.google.accompanist.permissions.shouldShowRationale
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import javax.inject.Inject
|
||||
|
|
@ -24,9 +21,6 @@ class SettingsViewModel @Inject constructor(
|
|||
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(SettingsState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var notificationsPermissionJob: Job? = null
|
||||
|
||||
fun onEvent(event: SettingsEvent) {
|
||||
|
|
@ -77,17 +71,6 @@ class SettingsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showReaderTranslatorLanguageBottomSheet = event.show
|
||||
?: !it.showReaderTranslatorLanguageBottomSheet,
|
||||
readerTranslatorLanguageBottomSheetTranslateFrom = event.translateFrom
|
||||
?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -32,35 +31,25 @@ import ua.acclorite.book_story.domain.util.OnNavigate
|
|||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||
import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.TranslatorLanguageBottomSheet
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.translator_bottom_sheet.ReaderTranslatorBottomSheetLanguageChooser
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun ReaderSettingsRoot() {
|
||||
val navigator = LocalNavigator.current
|
||||
val mainViewModel: MainViewModel = hiltViewModel()
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
|
||||
val state = mainViewModel.state.collectAsState()
|
||||
val settingsState = settingsViewModel.state.collectAsState()
|
||||
|
||||
ReaderSettings(
|
||||
state = state,
|
||||
settingsState = settingsState,
|
||||
onNavigate = { navigator.it() },
|
||||
onMainEvent = mainViewModel::onEvent,
|
||||
onEvent = settingsViewModel::onEvent
|
||||
onMainEvent = mainViewModel::onEvent
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,70 +57,16 @@ fun ReaderSettingsRoot() {
|
|||
@Composable
|
||||
private fun ReaderSettings(
|
||||
state: State<MainState>,
|
||||
settingsState: State<SettingsState>,
|
||||
onNavigate: OnNavigate,
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
onEvent: (SettingsEvent) -> Unit
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
) {
|
||||
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
|
||||
val context = LocalContext.current
|
||||
|
||||
val fontFamily = remember(state.value.fontFamily) {
|
||||
Constants.FONTS.find {
|
||||
it.id == state.value.fontFamily
|
||||
} ?: Constants.FONTS[0]
|
||||
}
|
||||
val translateFrom = remember(state.value.translateFrom) {
|
||||
if (state.value.translateFrom!! == "auto") {
|
||||
return@remember context.getString(R.string.translator_auto_short)
|
||||
}
|
||||
|
||||
val locale = Locale(state.value.translateFrom!!)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
val translateTo = remember(state.value.translateTo) {
|
||||
val locale = Locale(state.value.translateTo!!)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsState.value.showReaderTranslatorLanguageBottomSheet) {
|
||||
TranslatorLanguageBottomSheet(
|
||||
selectedLanguage = if (
|
||||
settingsState.value.readerTranslatorLanguageBottomSheetTranslateFrom
|
||||
) {
|
||||
state.value.translateFrom!!
|
||||
} else state.value.translateTo!!,
|
||||
|
||||
unselectedLanguage = if (
|
||||
!settingsState.value.readerTranslatorLanguageBottomSheetTranslateFrom
|
||||
) {
|
||||
state.value.translateFrom!!
|
||||
} else state.value.translateTo!!,
|
||||
|
||||
translateFromSelecting = settingsState.value.readerTranslatorLanguageBottomSheetTranslateFrom,
|
||||
onSelect = { from, to ->
|
||||
onMainEvent(MainEvent.OnChangeTranslateFrom(from))
|
||||
onMainEvent(MainEvent.OnChangeTranslateTo(to))
|
||||
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
},
|
||||
onDismiss = {
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
@ -164,91 +99,6 @@ private fun ReaderSettings(
|
|||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.translator_reader_settings),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = state.value.enableTranslator!!,
|
||||
title = stringResource(id = R.string.translator_enable_default_option),
|
||||
description = stringResource(id = R.string.translator_enable_default_option_desc),
|
||||
onClick = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeEnableTranslator(
|
||||
!state.value.enableTranslator!!
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.translator_language_option)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
ReaderTranslatorBottomSheetLanguageChooser(
|
||||
fromLanguage = translateFrom,
|
||||
toLanguage = translateTo,
|
||||
onFromClick = {
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = true,
|
||||
translateFrom = true
|
||||
)
|
||||
)
|
||||
},
|
||||
onToClick = {
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = true,
|
||||
translateFrom = false
|
||||
)
|
||||
)
|
||||
},
|
||||
switchEnabled = state.value.translateFrom != "auto",
|
||||
onSwitchClick = {
|
||||
val from = state.value.translateFrom!!
|
||||
val to = state.value.translateTo!!
|
||||
|
||||
onMainEvent(MainEvent.OnChangeTranslateFrom(to))
|
||||
onMainEvent(MainEvent.OnChangeTranslateTo(from))
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = state.value.doubleClickTranslation!!,
|
||||
title = stringResource(id = R.string.translator_double_click_to_translate_option),
|
||||
description = stringResource(id = R.string.translator_double_click_to_translate_option_desc),
|
||||
onClick = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeDoubleClickTranslation(
|
||||
!state.value.doubleClickTranslation!!
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.font_reader_settings),
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -36,8 +36,6 @@
|
|||
<string name="move_book">Перемістити книгу?</string>
|
||||
<string name="delete_history">Видалити історію читання?</string>
|
||||
<string name="confirm_update">Підтвердити оновлення?</string>
|
||||
<string name="download_language">Завантажити %1$s?</string>
|
||||
<string name="download_languages">Завантажити мови?</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
|
|
@ -79,17 +77,6 @@
|
|||
Натиснувши на кнопку «Завантажити», ви перейдете в ваш браузер
|
||||
та завантажете останнє оновлення автоматично.
|
||||
</string>
|
||||
<string name="download_language_description">
|
||||
Вам потрібно завантажити цю мову перед використанням.
|
||||
Після завантаження, вона буде доступна офлайн та не потребуватиме інтернету.
|
||||
Кожна мова має розмір близько ~30мб, для завантаження рекомендується використовувати Wi-Fi.
|
||||
</string>
|
||||
<string name="download_languages_description">
|
||||
Вам потрібно завантажити ці мови перед використанням.
|
||||
Кожна мова має розмір близько ~30мб, для завантаження рекомендується використовувати Wi-Fi.
|
||||
Ви можете вибрати мови, які ви хочете завантажити,
|
||||
всі не вибрані мови не будуть перекладені.
|
||||
</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
|
|
@ -116,12 +103,6 @@
|
|||
Щось пішло не так.
|
||||
Спробуйте пізніше.
|
||||
</string>
|
||||
<string name="error_language_not_identified">Мова не була ідентифікована.</string>
|
||||
<string name="error_languages_not_identified">Не вдалося ідентифікувати жодної мови.</string>
|
||||
<string name="error_cannot_select_language">Неможливо вибрати ту саму мову двічі.</string>
|
||||
<string name="error_wrong_language">Не можна вибрати цю мову.</string>
|
||||
<string name="error_could_not_download_language">Не вдалося завантажити мову.</string>
|
||||
<string name="error_could_not_download_languages">Не вдалося завантажити мови.</string>
|
||||
<string name="error_could_not_reset_cover">Не вдалося скинути обкладинку.</string>
|
||||
<string name="error_closed_source">Закритий код</string>
|
||||
|
||||
|
|
@ -136,9 +117,6 @@
|
|||
Нехай ваша остання прочитана книга відкриє перед
|
||||
вами сторінки нової великої історії вашого читацького шляху
|
||||
</string>
|
||||
<string name="translator_languages_empty">
|
||||
Не існує мови з такою назвою, або вона не підтримується
|
||||
</string>
|
||||
|
||||
<!-- Query strings -->
|
||||
<string name="selected_items_count_query">%1$s обрано</string>
|
||||
|
|
@ -173,14 +151,11 @@
|
|||
<string name="no">Ні</string>
|
||||
<string name="download">Завантажити</string>
|
||||
<string name="done">Готово</string>
|
||||
<string name="undo_translation">Відмінити</string>
|
||||
<string name="continue_download">Далі</string>
|
||||
|
||||
<!-- Items -->
|
||||
<string name="files">файли</string>
|
||||
<string name="books">книги</string>
|
||||
<string name="history">історію</string>
|
||||
<string name="languages">мови</string>
|
||||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Невідомий</string>
|
||||
|
|
@ -194,8 +169,6 @@
|
|||
<string name="delete_cover">Видалити обкладинку</string>
|
||||
<string name="delete_cover_desc">Видаліть нинішню обкладинку книги</string>
|
||||
<string name="author">Автор</string>
|
||||
<string name="description">Опис</string>
|
||||
<string name="text">Текст</string>
|
||||
|
||||
<!-- Tabs -->
|
||||
<string name="reading_tab">Читаю</string>
|
||||
|
|
@ -237,13 +210,8 @@
|
|||
<string name="check_for_updates_option">Перевіряти на оновлення</string>
|
||||
<string name="check_for_updates_option_desc">Перевіряти на новішу версію застосунку при запуску</string>
|
||||
<string name="side_padding_option">Бокові відступи</string>
|
||||
<string name="translator_language_option">Мова перекладача</string>
|
||||
<string name="translator_enable_default_option">Увімкнути перекладач</string>
|
||||
<string name="translator_enable_default_option_desc">Увімкнути перекладач при додаванні книги за замовчуванням</string>
|
||||
<string name="translator_double_click_to_translate_option">Переклад подвійним кліком</string>
|
||||
<string name="translator_double_click_to_translate_option_desc">Двічі клацніть на абзац, щоб перекласти його</string>
|
||||
<string name="translator_translate_on_start_option">Перекладати при відкритті книги</string>
|
||||
<string name="translator_translate_on_start_option_desc">Автоматично перекладати весь текст при відкритті книги</string>
|
||||
|
||||
<!-- Colors -->
|
||||
<string name="red_color">Червоний</string>
|
||||
|
|
@ -291,11 +259,6 @@
|
|||
<string name="copied">Скопійовано</string>
|
||||
<string name="history_deleted">Історія була успішно видалена.</string>
|
||||
<string name="no_updates">Остання версія застосунка вже встановлена.</string>
|
||||
<string name="language_downloaded">Мова була успішно завантажена та вибрана.</string>
|
||||
<string name="languages_downloaded">Всі необхідні мови завантажені. Переклад почнеться незабаром.</string>
|
||||
<string name="translator_canceling">Переклад відміняється. Будь ласка, зачекайте.</string>
|
||||
<string name="translator_text_canceling">Перекладений текст відміняється. Будь ласка, зачекайте.</string>
|
||||
<string name="translator_starting">Переклад скоро почнеться. Будь ласка, зачекайте.</string>
|
||||
<string name="cover_reset">Обкладинка була успішно скинута.</string>
|
||||
|
||||
<!-- Snackbar messages -->
|
||||
|
|
@ -325,17 +288,6 @@
|
|||
</string>
|
||||
<string name="letters_and_words">%1$s букв(а), %2$s слів.</string>
|
||||
|
||||
<!-- Translator -->
|
||||
<string name="translator_auto">Визначити мову</string>
|
||||
<string name="translator_auto_short">Визначити</string>
|
||||
<string name="translator_query">Перекласти %1$s</string>
|
||||
<string name="translator_from">з</string>
|
||||
<string name="translator_to">на</string>
|
||||
<string name="translator_all_languages">Всі мови</string>
|
||||
<string name="translator_recent_languages">Недавні мови</string>
|
||||
<string name="translator_occurrences">%1$s абзац(ів)</string>
|
||||
<string name="translator_show_original">Показати оригінал (%1$s на %2$s)</string>
|
||||
|
||||
<!-- About -->
|
||||
<string name="app_version_option">Версія застосунка</string>
|
||||
<string name="app_version_option_desc_1">Book\'s Story v%1$s</string>
|
||||
|
|
@ -577,8 +529,5 @@
|
|||
<string name="reset_start_content_desc">Скинути початковий посібник</string>
|
||||
<string name="exit_editing_content_desc">Вийти з режиму редагування</string>
|
||||
<string name="open_in_web_content_desc">Відкрити в браузері</string>
|
||||
<string name="translator_content_desc">Перекладач</string>
|
||||
<string name="translator_language_content_desc">Поміняти мови місцями</string>
|
||||
<string name="downloaded_content_desc">Завантажено</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -55,8 +55,6 @@
|
|||
<string name="move_book">Move book?</string>
|
||||
<string name="delete_history">Delete reading history?</string>
|
||||
<string name="confirm_update">Confirm update?</string>
|
||||
<string name="download_language">Download %1$s?</string>
|
||||
<string name="download_languages">Download languages?</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
|
|
@ -98,17 +96,6 @@
|
|||
By clicking the «Download» button you will be transferred
|
||||
to your browser and download the latest update automatically.
|
||||
</string>
|
||||
<string name="download_language_description">
|
||||
You need to download this language before using it.
|
||||
After downloading, it will be available offline and will not require internet.
|
||||
Each language is about ~30mb in size, it is recommended to use Wi-Fi to download it.
|
||||
</string>
|
||||
<string name="download_languages_description">
|
||||
You need to download these languages before.
|
||||
Each language is about ~30mb in size, it is recommended to use Wi-Fi to download it.
|
||||
You can select which languages you want to download,
|
||||
all unselected languages will not be translated.
|
||||
</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
|
|
@ -135,12 +122,6 @@
|
|||
Something went wrong.
|
||||
Try again later.
|
||||
</string>
|
||||
<string name="error_language_not_identified">Could not identify language.</string>
|
||||
<string name="error_languages_not_identified">Could not identify any languages.</string>
|
||||
<string name="error_cannot_select_language">Cannot select same language twice.</string>
|
||||
<string name="error_wrong_language">Cannot select this language.</string>
|
||||
<string name="error_could_not_download_language">Could not download language.</string>
|
||||
<string name="error_could_not_download_languages">Could not download languages.</string>
|
||||
<string name="error_could_not_reset_cover">Could not reset cover image.</string>
|
||||
<string name="error_closed_source">Closed source</string>
|
||||
|
||||
|
|
@ -154,9 +135,6 @@
|
|||
<string name="history_empty">
|
||||
Let your last book you read open the pages of a new great story of your reading journey
|
||||
</string>
|
||||
<string name="translator_languages_empty">
|
||||
There is no such language with this name, or it is not supported
|
||||
</string>
|
||||
|
||||
<!-- Query strings -->
|
||||
<string name="selected_items_count_query">%1$s selected</string>
|
||||
|
|
@ -191,14 +169,11 @@
|
|||
<string name="no">No</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="undo_translation">Undo</string>
|
||||
<string name="continue_download">Continue</string>
|
||||
|
||||
<!-- Items -->
|
||||
<string name="files">files</string>
|
||||
<string name="books">books</string>
|
||||
<string name="history">history</string>
|
||||
<string name="languages">languages</string>
|
||||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Unknown</string>
|
||||
|
|
@ -212,8 +187,6 @@
|
|||
<string name="delete_cover">Delete cover image</string>
|
||||
<string name="delete_cover_desc">Delete current cover image of the book</string>
|
||||
<string name="author">Author</string>
|
||||
<string name="description">Description</string>
|
||||
<string name="text">Text</string>
|
||||
|
||||
<!-- Tabs -->
|
||||
<string name="reading_tab">Reading</string>
|
||||
|
|
@ -255,13 +228,8 @@
|
|||
<string name="check_for_updates_option">Check for updates</string>
|
||||
<string name="check_for_updates_option_desc">Check for newer app version on startup</string>
|
||||
<string name="side_padding_option">Side padding</string>
|
||||
<string name="translator_language_option">Translator language</string>
|
||||
<string name="translator_enable_default_option">Enable translator</string>
|
||||
<string name="translator_enable_default_option_desc">Enable translator when adding a book by default</string>
|
||||
<string name="translator_double_click_to_translate_option">Double click translation</string>
|
||||
<string name="translator_double_click_to_translate_option_desc">Double click on paragraph to translate it</string>
|
||||
<string name="translator_translate_on_start_option">Translate when open a book</string>
|
||||
<string name="translator_translate_on_start_option_desc">Automatically translate the whole text when open a book</string>
|
||||
|
||||
<!-- Colors -->
|
||||
<string name="red_color">Red</string>
|
||||
|
|
@ -309,13 +277,6 @@
|
|||
<string name="copied">Copied</string>
|
||||
<string name="history_deleted">History was successfully deleted.</string>
|
||||
<string name="no_updates">The latest version of the app is already installed.</string>
|
||||
<string name="language_downloaded">Language was successfully downloaded and selected.</string>
|
||||
<string name="languages_downloaded">
|
||||
All required languages are downloaded. Translation will start soon.
|
||||
</string>
|
||||
<string name="translator_canceling">Translation is canceling. Please wait.</string>
|
||||
<string name="translator_text_canceling">Translated text is canceling. Please wait.</string>
|
||||
<string name="translator_starting">Translation will start soon. Please wait.</string>
|
||||
<string name="cover_reset">Cover image was successfully reset.</string>
|
||||
|
||||
<!-- Snackbar messages -->
|
||||
|
|
@ -345,17 +306,6 @@
|
|||
</string>
|
||||
<string name="letters_and_words">%1$s characters, %2$s words.</string>
|
||||
|
||||
<!-- Translator -->
|
||||
<string name="translator_auto">Detect language</string>
|
||||
<string name="translator_auto_short">Detect</string>
|
||||
<string name="translator_query">Translate %1$s</string>
|
||||
<string name="translator_from">from</string>
|
||||
<string name="translator_to">to</string>
|
||||
<string name="translator_all_languages">All languages</string>
|
||||
<string name="translator_recent_languages">Recent languages</string>
|
||||
<string name="translator_occurrences">%1$s paragraph(s)</string>
|
||||
<string name="translator_show_original">Show original (%1$s to %2$s)</string>
|
||||
|
||||
<!-- About -->
|
||||
<string name="app_version_option">App version</string>
|
||||
<string name="app_version_option_desc_1">Book\'s Story v%1$s</string>
|
||||
|
|
@ -598,8 +548,5 @@
|
|||
<string name="reset_start_content_desc">Reset start guide</string>
|
||||
<string name="exit_editing_content_desc">Exit editing mode</string>
|
||||
<string name="open_in_web_content_desc">Open in web</string>
|
||||
<string name="translator_content_desc">Translator</string>
|
||||
<string name="translator_language_content_desc">Change language places</string>
|
||||
<string name="downloaded_content_desc">Downloaded</string>
|
||||
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue