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:
acclorite 2024-07-15 00:50:28 +03:00
parent e0d04b2c7d
commit 1e42c258e7
66 changed files with 256 additions and 4376 deletions

View file

@ -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")
}

View file

@ -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')"
]
}
}

View file

@ -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"

View file

@ -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()
)
}
}

View file

@ -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(

View file

@ -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
)

View file

@ -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
)

View file

@ -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>
}

View file

@ -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
}

View file

@ -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
)
}
}

View file

@ -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
}

View file

@ -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
)
}
}

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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)
}
)
}
}

View file

@ -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
)

View file

@ -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>?
)

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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>)
}

View file

@ -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
)
}
}

View file

@ -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
)
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
)
}
}

View file

@ -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)
}
}

View file

@ -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
)
}

View file

@ -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")
}

View file

@ -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

View file

@ -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)
)
}
}
}
}
}
}

View file

@ -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
)
}

View file

@ -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
)
)
}
)
}
}
}
}
}

View file

@ -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)
)
)
}
)
}

View file

@ -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()
}

View file

@ -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,
)

View file

@ -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()
)
}
)
}
}
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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
)
}

View file

@ -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)

View file

@ -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)
)
}

View file

@ -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
}

View file

@ -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
)
}

View file

@ -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
)
)
}
}

View file

@ -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,

View file

@ -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)
)
}
)
}
}
)
}

View file

@ -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))
}
}
}
}
}
}

View file

@ -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
)
)
}
}

View file

@ -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()
}
}
)
}
}
}
)
)
}
}

View file

@ -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
)
)
}
}
}
}

View file

@ -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
)
}
}

View file

@ -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,

View file

@ -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,
)

View file

@ -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,

View file

@ -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()
}

View file

@ -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,
)

View file

@ -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
)
}
}
}
}
}

View file

@ -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

View file

@ -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>

View file

@ -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>