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