Built-in Translator + ModalBottomSheet → CustomBottomSheet(fixes bugs) + Constants.NAVIGATION_ITEMS for NavigationBars + Various dependencies updates + ReleaseResponse → LatestReleaseInfo + Fixed: Modifier.clip did not apply correctly + Room Version Migration(cause of translator) + Changed order of the selection toolbar items(revert): Translate, Copy, Dictionary > Copy, Translate, Dictionary + Gradle Version Update 8.2 → 8.4 + StringWithId → LineWithTranslation + Less transparent bars color in ReaderScreen + Small localization changes (v1.0.1)
This commit is contained in:
parent
b5d99916a1
commit
04e67aa03c
101 changed files with 5120 additions and 724 deletions
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="KotlinJpsPluginSettings">
|
<component name="KotlinJpsPluginSettings">
|
||||||
<option name="version" value="1.9.21" />
|
<option name="version" value="2.0.0" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
id("com.google.dagger.hilt.android")
|
id("com.google.dagger.hilt.android")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
|
id("androidx.room")
|
||||||
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -24,6 +27,10 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
room {
|
||||||
|
schemaDirectory("$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
getByName("debug") {
|
getByName("debug") {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
|
@ -76,12 +83,12 @@ aboutLibraries {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
// Default
|
// Default
|
||||||
implementation("androidx.core:core-ktx:1.13.0")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1")
|
||||||
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.04.01"))
|
implementation(platform("androidx.compose:compose-bom:2024.05.00"))
|
||||||
implementation("androidx.compose.foundation:foundation")
|
implementation("androidx.compose.foundation:foundation")
|
||||||
implementation("androidx.compose.animation:animation")
|
implementation("androidx.compose.animation:animation")
|
||||||
implementation("androidx.compose.animation:animation-android")
|
implementation("androidx.compose.animation:animation-android")
|
||||||
|
|
@ -90,18 +97,18 @@ dependencies {
|
||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.ui:ui-android")
|
implementation("androidx.compose.ui:ui-android")
|
||||||
implementation("androidx.compose.material3:material3:latest.release")
|
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")
|
||||||
implementation("androidx.compose.material:material-icons-extended")
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
implementation("androidx.compose.material:material")
|
implementation("androidx.compose.material:material")
|
||||||
|
|
||||||
// All dependencies
|
// All dependencies
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1")
|
||||||
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
|
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
|
||||||
|
|
||||||
// Dagger - Hilt
|
// Dagger - Hilt
|
||||||
implementation("com.google.dagger:hilt-android:2.51.1")
|
implementation("com.google.dagger:hilt-android:2.51.1")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling:1.6.6")
|
debugImplementation("androidx.compose.ui:ui-tooling:1.6.7")
|
||||||
ksp("com.google.dagger:hilt-android-compiler:2.51")
|
ksp("com.google.dagger:hilt-android-compiler:2.51")
|
||||||
implementation("com.google.dagger:hilt-compiler:latest.release")
|
implementation("com.google.dagger:hilt-compiler:latest.release")
|
||||||
ksp("androidx.hilt:hilt-compiler:latest.release")
|
ksp("androidx.hilt:hilt-compiler:latest.release")
|
||||||
|
|
@ -152,4 +159,11 @@ 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")
|
||||||
|
implementation("com.google.mlkit:language-id:latest.release")
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||||
}
|
}
|
||||||
|
|
@ -25,31 +25,23 @@ import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||||
import ua.acclorite.book_story.presentation.data.NavigationHost
|
import ua.acclorite.book_story.presentation.data.NavigationHost
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.presentation.screens.about.AboutScreenRoot
|
import ua.acclorite.book_story.presentation.screens.about.AboutScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.screens.about.nested.credits.CreditsScreenRoot
|
import ua.acclorite.book_story.presentation.screens.about.nested.credits.CreditsScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.about.nested.license_info.LicenseInfoScreenRoot
|
import ua.acclorite.book_story.presentation.screens.about.nested.license_info.LicenseInfoScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.about.nested.license_info.data.LicenseInfoViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.screens.about.nested.licenses.LicensesScreenRoot
|
import ua.acclorite.book_story.presentation.screens.about.nested.licenses.LicensesScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.about.nested.licenses.data.LicensesViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreenRoot
|
import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreenRoot
|
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.help.HelpScreenRoot
|
import ua.acclorite.book_story.presentation.screens.help.HelpScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.screens.history.HistoryScreenRoot
|
import ua.acclorite.book_story.presentation.screens.history.HistoryScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.library.LibraryScreenRoot
|
import ua.acclorite.book_story.presentation.screens.library.LibraryScreenRoot
|
||||||
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.ReaderScreenRoot
|
import ua.acclorite.book_story.presentation.screens.reader.ReaderScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot
|
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot
|
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettingsRoot
|
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettingsRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettingsRoot
|
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettingsRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.start.StartScreenRoot
|
import ua.acclorite.book_story.presentation.screens.start.StartScreenRoot
|
||||||
import ua.acclorite.book_story.presentation.screens.start.data.StartViewModel
|
|
||||||
import ua.acclorite.book_story.presentation.ui.BookStoryTheme
|
import ua.acclorite.book_story.presentation.ui.BookStoryTheme
|
||||||
import ua.acclorite.book_story.presentation.ui.Transitions
|
import ua.acclorite.book_story.presentation.ui.Transitions
|
||||||
import ua.acclorite.book_story.presentation.ui.isDark
|
import ua.acclorite.book_story.presentation.ui.isDark
|
||||||
|
|
@ -66,14 +58,6 @@ class Activity : AppCompatActivity() {
|
||||||
private val libraryViewModel: LibraryViewModel by viewModels()
|
private val libraryViewModel: LibraryViewModel by viewModels()
|
||||||
private val historyViewModel: HistoryViewModel by viewModels()
|
private val historyViewModel: HistoryViewModel by viewModels()
|
||||||
private val browseViewModel: BrowseViewModel by viewModels()
|
private val browseViewModel: BrowseViewModel by viewModels()
|
||||||
private val bookInfoViewModel: BookInfoViewModel by viewModels()
|
|
||||||
private val readerViewModel: ReaderViewModel by viewModels()
|
|
||||||
private val helpViewModel: HelpViewModel by viewModels()
|
|
||||||
private val aboutViewModel: AboutViewModel by viewModels()
|
|
||||||
private val licenseInfoViewModel: LicenseInfoViewModel by viewModels()
|
|
||||||
private val licensesViewModel: LicensesViewModel by viewModels()
|
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
|
||||||
private val startViewModel: StartViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -102,8 +86,8 @@ class Activity : AppCompatActivity() {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val isLoaded by mainViewModel.isReady.collectAsState()
|
val isLoaded = mainViewModel.isReady.collectAsState()
|
||||||
val state by mainViewModel.state.collectAsState()
|
val state = mainViewModel.state.collectAsState()
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
|
|
@ -120,15 +104,15 @@ class Activity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoaded) {
|
if (isLoaded.value) {
|
||||||
BookStoryTheme(
|
BookStoryTheme(
|
||||||
theme = state.theme!!,
|
theme = state.value.theme!!,
|
||||||
isDark = state.darkTheme!!.isDark(),
|
isDark = state.value.darkTheme!!.isDark(),
|
||||||
isPureDark = state.pureDark!!.isPureDark(this),
|
isPureDark = state.value.pureDark!!.isPureDark(this),
|
||||||
themeContrast = state.themeContrast!!
|
themeContrast = state.value.themeContrast!!
|
||||||
) {
|
) {
|
||||||
NavigationHost(
|
NavigationHost(
|
||||||
startScreen = if (state.showStartScreen!!) Screen.START
|
startScreen = if (state.value.showStartScreen!!) Screen.START
|
||||||
else Screen.LIBRARY
|
else Screen.LIBRARY
|
||||||
) {
|
) {
|
||||||
navigation(
|
navigation(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ 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
|
||||||
|
|
@ -41,4 +45,20 @@ object AppModule {
|
||||||
"book_db"
|
"book_db"
|
||||||
).allowMainThreadQueries().build().dao
|
).allowMainThreadQueries().build().dao
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideModelManager(): RemoteModelManager {
|
||||||
|
return RemoteModelManager.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideLanguageIdentifier(): LanguageIdentifier {
|
||||||
|
return LanguageIdentification.getClient(
|
||||||
|
LanguageIdentificationOptions.Builder()
|
||||||
|
.setConfidenceThreshold(0.2f)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
|
@ -16,5 +17,10 @@ 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 +1,10 @@
|
||||||
package ua.acclorite.book_story.data.local.notification
|
package ua.acclorite.book_story.data.local.notification
|
||||||
|
|
||||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||||
|
|
||||||
interface UpdatesNotificationService {
|
interface UpdatesNotificationService {
|
||||||
|
|
||||||
fun postNotification(releaseInfo: ReleaseResponse)
|
fun postNotification(releaseInfo: LatestReleaseInfo)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = "updates_channel"
|
const val CHANNEL_ID = "updates_channel"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UpdatesNotificationServiceImpl @Inject constructor(
|
class UpdatesNotificationServiceImpl @Inject constructor(
|
||||||
|
|
@ -18,7 +18,7 @@ class UpdatesNotificationServiceImpl @Inject constructor(
|
||||||
private val notificationManager = application.getSystemService(Context.NOTIFICATION_SERVICE)
|
private val notificationManager = application.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||||
as NotificationManager
|
as NotificationManager
|
||||||
|
|
||||||
override fun postNotification(releaseInfo: ReleaseResponse) {
|
override fun postNotification(releaseInfo: LatestReleaseInfo) {
|
||||||
val downloadLatestVersionIntent = Intent(
|
val downloadLatestVersionIntent = Intent(
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
Uri.parse(application.getString(R.string.download_latest_release_page))
|
Uri.parse(application.getString(R.string.download_latest_release_page))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package ua.acclorite.book_story.data.local.room
|
package ua.acclorite.book_story.data.local.room
|
||||||
|
|
||||||
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||||
|
|
@ -10,8 +11,11 @@ import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
||||||
BookEntity::class,
|
BookEntity::class,
|
||||||
HistoryEntity::class
|
HistoryEntity::class
|
||||||
],
|
],
|
||||||
version = 1,
|
version = 2,
|
||||||
exportSchema = false
|
autoMigrations = [
|
||||||
|
AutoMigration(1, 2)
|
||||||
|
],
|
||||||
|
exportSchema = true
|
||||||
)
|
)
|
||||||
abstract class BookDatabase : RoomDatabase() {
|
abstract class BookDatabase : RoomDatabase() {
|
||||||
abstract val dao: BookDao
|
abstract val dao: BookDao
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,12 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,7 +44,12 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
filePath = bookEntity.filePath,
|
filePath = bookEntity.filePath,
|
||||||
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,9 +1,8 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser
|
||||||
|
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
import ua.acclorite.book_story.domain.util.Resource
|
import ua.acclorite.book_story.domain.util.Resource
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface TextParser {
|
interface TextParser {
|
||||||
suspend fun parse(file: File): Resource<List<StringWithId>>
|
suspend fun parse(file: File): Resource<List<String>>
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.parser.epub.EpubTextParser
|
import ua.acclorite.book_story.data.parser.epub.EpubTextParser
|
||||||
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
|
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
|
||||||
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
|
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
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 java.io.File
|
import java.io.File
|
||||||
|
|
@ -15,7 +14,7 @@ class TextParserImpl @Inject constructor(
|
||||||
private val pdfTextParser: PdfTextParser,
|
private val pdfTextParser: PdfTextParser,
|
||||||
private val epubTextParser: EpubTextParser,
|
private val epubTextParser: EpubTextParser,
|
||||||
) : TextParser {
|
) : TextParser {
|
||||||
override suspend fun parse(file: File): Resource<List<StringWithId>> {
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
val fileFormat = ".${file.name.substringAfterLast(".")}"
|
val fileFormat = ".${file.name.substringAfterLast(".")}"
|
||||||
|
|
||||||
if (fileFormat == ".pdf") {
|
if (fileFormat == ".pdf") {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,12 @@ 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) {
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import org.jsoup.nodes.Document.OutputSettings
|
||||||
import org.jsoup.safety.Safelist
|
import org.jsoup.safety.Safelist
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.parser.TextParser
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
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 java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
@ -21,7 +20,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class EpubTextParser @Inject constructor() : TextParser {
|
class EpubTextParser @Inject constructor() : TextParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): Resource<List<StringWithId>> {
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
if (!file.name.endsWith(".epub")) {
|
if (!file.name.endsWith(".epub")) {
|
||||||
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +56,7 @@ class EpubTextParser @Inject constructor() : TextParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val stringWithIds = mutableListOf<StringWithId>()
|
val strings = mutableListOf<String>()
|
||||||
|
|
||||||
val parsedText = Jsoup.parse(unformattedText.toString())
|
val parsedText = Jsoup.parse(unformattedText.toString())
|
||||||
parsedText.outputSettings(OutputSettings().prettyPrint(false))
|
parsedText.outputSettings(OutputSettings().prettyPrint(false))
|
||||||
|
|
@ -78,15 +77,15 @@ class EpubTextParser @Inject constructor() : TextParser {
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.forEach {
|
.forEach {
|
||||||
if (it.isNotBlank()) {
|
if (it.isNotBlank()) {
|
||||||
stringWithIds.add(StringWithId(it.trim()))
|
strings.add(it.trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringWithIds.isEmpty()) {
|
if (strings.isEmpty()) {
|
||||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Resource.Success(stringWithIds)
|
return Resource.Success(strings)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return Resource.Error(
|
return Resource.Error(
|
||||||
UIText.StringResource(
|
UIText.StringResource(
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,12 @@ 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) {
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||||
import com.tom_roush.pdfbox.text.PDFTextStripper
|
import com.tom_roush.pdfbox.text.PDFTextStripper
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.parser.TextParser
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
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 java.io.File
|
import java.io.File
|
||||||
|
|
@ -14,7 +13,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class PdfTextParser @Inject constructor(private val application: Application) : TextParser {
|
class PdfTextParser @Inject constructor(private val application: Application) : TextParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): Resource<List<StringWithId>> {
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
if (!file.name.endsWith(".pdf")) {
|
if (!file.name.endsWith(".pdf")) {
|
||||||
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +22,7 @@ class PdfTextParser @Inject constructor(private val application: Application) :
|
||||||
PDFBoxResourceLoader.init(application)
|
PDFBoxResourceLoader.init(application)
|
||||||
|
|
||||||
val document = PDDocument.load(file)
|
val document = PDDocument.load(file)
|
||||||
val stringWithIds = mutableListOf<StringWithId>()
|
val strings = mutableListOf<String>()
|
||||||
|
|
||||||
val pdfStripper = PDFTextStripper()
|
val pdfStripper = PDFTextStripper()
|
||||||
pdfStripper.paragraphStart = "</br>"
|
pdfStripper.paragraphStart = "</br>"
|
||||||
|
|
@ -103,14 +102,14 @@ class PdfTextParser @Inject constructor(private val application: Application) :
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.forEach { line ->
|
lines.forEach { line ->
|
||||||
stringWithIds.add(StringWithId(line.trim()))
|
strings.add(line.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringWithIds.isEmpty()) {
|
if (strings.isEmpty()) {
|
||||||
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Resource.Success(stringWithIds)
|
return Resource.Success(strings)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return Resource.Error(
|
return Resource.Error(
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,12 @@ 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) {
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers
|
||||||
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.parser.TextParser
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
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 java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
@ -14,19 +13,19 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class TxtTextParser @Inject constructor() : TextParser {
|
class TxtTextParser @Inject constructor() : TextParser {
|
||||||
|
|
||||||
override suspend fun parse(file: File): Resource<List<StringWithId>> {
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
if (!file.name.endsWith(".txt")) {
|
if (!file.name.endsWith(".txt")) {
|
||||||
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val formattedLines = mutableListOf<StringWithId>()
|
val formattedLines = mutableListOf<String>()
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
BufferedReader(FileReader(file)).forEachLine { line ->
|
BufferedReader(FileReader(file)).forEachLine { line ->
|
||||||
if (line.isNotBlank()) {
|
if (line.isNotBlank()) {
|
||||||
formattedLines.add(
|
formattedLines.add(
|
||||||
StringWithId(line)
|
line.trim()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package ua.acclorite.book_story.data.remote
|
package ua.acclorite.book_story.data.remote
|
||||||
|
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||||
|
|
||||||
interface GithubAPI {
|
interface GithubAPI {
|
||||||
@GET("repos/Acclorite/book-story/releases/latest")
|
@GET("repos/Acclorite/book-story/releases/latest")
|
||||||
suspend fun getLatestRelease(): ReleaseResponse
|
suspend fun getLatestRelease(): LatestReleaseInfo
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val BASE_URL = "https://api.github.com/"
|
const val BASE_URL = "https://api.github.com/"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import com.squareup.moshi.JsonClass
|
||||||
@Keep
|
@Keep
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ReleaseResponse(
|
data class LatestReleaseInfo(
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
@Json(name = "tag_name") val tagName: String
|
@Json(name = "tag_name") val tagName: String
|
||||||
)
|
)
|
||||||
|
|
@ -6,6 +6,13 @@ import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
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
|
||||||
|
|
@ -24,28 +31,35 @@ import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||||
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.ReleaseResponse
|
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.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
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.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")
|
||||||
@Singleton
|
@Singleton
|
||||||
class BookRepositoryImpl @Inject constructor(
|
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,
|
||||||
|
|
@ -88,7 +102,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getBookText(textPath: String): List<StringWithId> {
|
override suspend fun getBookText(textPath: String): List<String> {
|
||||||
val textFile = File(textPath)
|
val textFile = File(textPath)
|
||||||
|
|
||||||
if (textPath.isBlank() || !textFile.exists()) {
|
if (textPath.isBlank() || !textFile.exists()) {
|
||||||
|
|
@ -108,7 +122,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
override suspend fun insertBook(
|
override suspend fun insertBook(
|
||||||
book: Book,
|
book: Book,
|
||||||
coverImage: CoverImage?,
|
coverImage: CoverImage?,
|
||||||
text: List<StringWithId>
|
text: List<String>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val filesDir = application.filesDir
|
val filesDir = application.filesDir
|
||||||
val coversDir = File(filesDir, "covers")
|
val coversDir = File(filesDir, "covers")
|
||||||
|
|
@ -135,7 +149,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
FileOutputStream(textPath).use { stream ->
|
FileOutputStream(textPath).use { stream ->
|
||||||
text.forEach { line ->
|
text.forEach { line ->
|
||||||
stream.write(line.line.toByteArray())
|
stream.write(line.toByteArray())
|
||||||
stream.write(System.lineSeparator().toByteArray())
|
stream.write(System.lineSeparator().toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,12 +183,34 @@ 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)
|
||||||
|
|
@ -197,7 +233,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateBookWithText(book: Book, text: List<StringWithId>): Boolean {
|
override suspend fun updateBookWithText(book: Book, text: List<String>): Boolean {
|
||||||
// without cover image
|
// without cover image
|
||||||
val filesDir = application.filesDir
|
val filesDir = application.filesDir
|
||||||
val booksDir = File(filesDir, "books")
|
val booksDir = File(filesDir, "books")
|
||||||
|
|
@ -220,7 +256,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
FileOutputStream(textPath).use { stream ->
|
FileOutputStream(textPath).use { stream ->
|
||||||
text.forEach { line ->
|
text.forEach { line ->
|
||||||
stream.write(line.line.toByteArray())
|
stream.write(line.toByteArray())
|
||||||
stream.write(System.lineSeparator().toByteArray())
|
stream.write(System.lineSeparator().toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -604,7 +640,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
database.deleteHistory(history.map { historyMapper.toHistoryEntity(it) })
|
database.deleteHistory(history.map { historyMapper.toHistoryEntity(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun checkForUpdates(postNotification: Boolean): ReleaseResponse? {
|
override suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val result = githubAPI.getLatestRelease()
|
val result = githubAPI.getLatestRelease()
|
||||||
|
|
@ -625,6 +661,155 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun isLanguageModelDownloaded(languageCode: String): Boolean {
|
||||||
|
val model = TranslateRemoteModel.Builder(languageCode).build()
|
||||||
|
return suspendCoroutine { 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 suspendCoroutine { continuation ->
|
||||||
|
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 suspendCoroutine { 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 suspendCoroutine { 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,10 @@ 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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
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>?
|
||||||
|
)
|
||||||
|
|
@ -3,11 +3,10 @@ package ua.acclorite.book_story.domain.model
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Immutable
|
@Immutable
|
||||||
data class StringWithId(
|
data class LanguageHistory(
|
||||||
val line: String,
|
val id: Int,
|
||||||
val id: String = UUID.randomUUID().toString()
|
val languageCode: String
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package ua.acclorite.book_story.domain.model
|
package ua.acclorite.book_story.domain.model
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class NavigationItem(
|
data class NavigationItem(
|
||||||
val title: String,
|
val screen: Screen,
|
||||||
val selectedIcon: Painter,
|
@StringRes val title: Int,
|
||||||
val unselectedIcon: Painter
|
@StringRes val tooltip: Int,
|
||||||
|
@DrawableRes val selectedIcon: Int,
|
||||||
|
@DrawableRes val unselectedIcon: Int
|
||||||
)
|
)
|
||||||
|
|
@ -8,14 +8,14 @@ import ua.acclorite.book_story.domain.util.UIText
|
||||||
sealed class NullableBook(
|
sealed class NullableBook(
|
||||||
val book: Book?,
|
val book: Book?,
|
||||||
val coverImage: CoverImage? = null,
|
val coverImage: CoverImage? = null,
|
||||||
val text: List<StringWithId> = emptyList(),
|
val text: List<String> = emptyList(),
|
||||||
val fileName: String?,
|
val fileName: String?,
|
||||||
val message: UIText?
|
val message: UIText?
|
||||||
) {
|
) {
|
||||||
class NotNull(
|
class NotNull(
|
||||||
book: Book,
|
book: Book,
|
||||||
coverImage: CoverImage?,
|
coverImage: CoverImage?,
|
||||||
text: List<StringWithId>
|
text: List<String>
|
||||||
) : NullableBook(
|
) : NullableBook(
|
||||||
book = book,
|
book = book,
|
||||||
text = text,
|
text = text,
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ package ua.acclorite.book_story.domain.repository
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
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.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
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
|
||||||
|
|
@ -25,12 +25,12 @@ interface BookRepository {
|
||||||
|
|
||||||
suspend fun getBookText(
|
suspend fun getBookText(
|
||||||
textPath: String
|
textPath: String
|
||||||
): List<StringWithId>
|
): List<String>
|
||||||
|
|
||||||
suspend fun insertBook(
|
suspend fun insertBook(
|
||||||
book: Book,
|
book: Book,
|
||||||
coverImage: CoverImage?,
|
coverImage: CoverImage?,
|
||||||
text: List<StringWithId>
|
text: List<String>
|
||||||
): Boolean
|
): Boolean
|
||||||
|
|
||||||
suspend fun updateBooks(
|
suspend fun updateBooks(
|
||||||
|
|
@ -39,7 +39,7 @@ interface BookRepository {
|
||||||
|
|
||||||
suspend fun updateBookWithText(
|
suspend fun updateBookWithText(
|
||||||
book: Book,
|
book: Book,
|
||||||
text: List<StringWithId>
|
text: List<String>
|
||||||
): Boolean
|
): Boolean
|
||||||
|
|
||||||
suspend fun updateCoverImageOfBook(
|
suspend fun updateCoverImageOfBook(
|
||||||
|
|
@ -51,6 +51,7 @@ interface BookRepository {
|
||||||
books: List<Book>
|
books: List<Book>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
suspend fun <T> retrieveDataFromDataStore(
|
suspend fun <T> retrieveDataFromDataStore(
|
||||||
key: Preferences.Key<T>,
|
key: Preferences.Key<T>,
|
||||||
defaultValue: T
|
defaultValue: T
|
||||||
|
|
@ -61,12 +62,14 @@ interface BookRepository {
|
||||||
value: T
|
value: T
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
suspend fun getAllSettings(scope: CoroutineScope): MainState
|
suspend fun getAllSettings(scope: CoroutineScope): MainState
|
||||||
|
|
||||||
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
||||||
|
|
||||||
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
||||||
|
|
||||||
|
|
||||||
suspend fun insertHistory(history: List<History>)
|
suspend fun insertHistory(history: List<History>)
|
||||||
|
|
||||||
suspend fun getHistory(): Flow<Resource<List<History>>>
|
suspend fun getHistory(): Flow<Resource<List<History>>>
|
||||||
|
|
@ -81,5 +84,25 @@ interface BookRepository {
|
||||||
history: List<History>
|
history: List<History>
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun checkForUpdates(postNotification: Boolean): ReleaseResponse?
|
|
||||||
|
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>
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package ua.acclorite.book_story.domain.use_case
|
package ua.acclorite.book_story.domain.use_case
|
||||||
|
|
||||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CheckForUpdates @Inject constructor(private val repository: BookRepository) {
|
class CheckForUpdates @Inject constructor(private val repository: BookRepository) {
|
||||||
|
|
||||||
suspend fun execute(postNotification: Boolean): ReleaseResponse? {
|
suspend fun execute(postNotification: Boolean): LatestReleaseInfo? {
|
||||||
return repository.checkForUpdates(postNotification)
|
return repository.checkForUpdates(postNotification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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 +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.StringWithId
|
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||||
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 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): List<StringWithId> {
|
suspend fun execute(textPath: String): Map<ID, LineWithTranslation> {
|
||||||
return repository.getBookText(textPath = textPath)
|
return repository.getBookText(textPath = textPath).withIndex().associate {
|
||||||
|
it.index to LineWithTranslation(originalLine = it.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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 +1,16 @@
|
||||||
package ua.acclorite.book_story.domain.use_case
|
package ua.acclorite.book_story.domain.use_case
|
||||||
|
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import ua.acclorite.book_story.domain.util.CoverImage
|
import ua.acclorite.book_story.domain.util.CoverImage
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class InsertBook @Inject constructor(private val repository: BookRepository) {
|
class InsertBook @Inject constructor(private val repository: BookRepository) {
|
||||||
suspend fun execute(book: Book, coverImage: CoverImage?, text: List<StringWithId>): Boolean {
|
suspend fun execute(
|
||||||
|
book: Book,
|
||||||
|
coverImage: CoverImage?,
|
||||||
|
text: List<String>
|
||||||
|
): Boolean {
|
||||||
return repository.insertBook(book, coverImage, text)
|
return repository.insertBook(book, coverImage, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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,13 +1,12 @@
|
||||||
package ua.acclorite.book_story.domain.use_case
|
package ua.acclorite.book_story.domain.use_case
|
||||||
|
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UpdateBookWithText @Inject constructor(private val repository: BookRepository) {
|
class UpdateBookWithText @Inject constructor(private val repository: BookRepository) {
|
||||||
|
|
||||||
suspend fun execute(book: Book, text: List<StringWithId>): Boolean {
|
suspend fun execute(book: Book, text: List<String>): Boolean {
|
||||||
return repository.updateBookWithText(book, text)
|
return repository.updateBookWithText(book, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,8 @@ 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.Credit
|
import ua.acclorite.book_story.domain.model.Credit
|
||||||
import ua.acclorite.book_story.domain.model.FontWithName
|
import ua.acclorite.book_story.domain.model.FontWithName
|
||||||
|
import ua.acclorite.book_story.domain.model.NavigationItem
|
||||||
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
import ua.acclorite.book_story.presentation.ui.Theme
|
import ua.acclorite.book_story.presentation.ui.Theme
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
|
|
@ -25,6 +27,31 @@ object Constants {
|
||||||
Pair("uk", "Українська"),
|
Pair("uk", "Українська"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Navigation items for NavigationBars.
|
||||||
|
val NAVIGATION_ITEMS = listOf(
|
||||||
|
NavigationItem(
|
||||||
|
screen = Screen.LIBRARY,
|
||||||
|
title = R.string.library_screen,
|
||||||
|
tooltip = R.string.library_content_desc,
|
||||||
|
selectedIcon = R.drawable.library_screen_filled,
|
||||||
|
unselectedIcon = R.drawable.library_screen_outlined
|
||||||
|
),
|
||||||
|
NavigationItem(
|
||||||
|
screen = Screen.HISTORY,
|
||||||
|
title = R.string.history_screen,
|
||||||
|
tooltip = R.string.history_content_desc,
|
||||||
|
selectedIcon = R.drawable.history_screen_filled,
|
||||||
|
unselectedIcon = R.drawable.history_screen_outlined
|
||||||
|
),
|
||||||
|
NavigationItem(
|
||||||
|
screen = Screen.BROWSE,
|
||||||
|
title = R.string.browse_screen,
|
||||||
|
tooltip = R.string.browse_content_desc,
|
||||||
|
selectedIcon = R.drawable.browse_screen_filled,
|
||||||
|
unselectedIcon = R.drawable.browse_screen_outlined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// Supported themes
|
// Supported themes
|
||||||
val THEMES = listOf(
|
val THEMES = listOf(
|
||||||
Pair(Theme.DYNAMIC, UIText.StringResource(R.string.dynamic_theme)),
|
Pair(Theme.DYNAMIC, UIText.StringResource(R.string.dynamic_theme)),
|
||||||
|
|
@ -255,6 +282,11 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
|
||||||
object DataStoreConstants {
|
object DataStoreConstants {
|
||||||
|
// App settings
|
||||||
val LANGUAGE = stringPreferencesKey("language")
|
val LANGUAGE = stringPreferencesKey("language")
|
||||||
val THEME = stringPreferencesKey("theme")
|
val THEME = stringPreferencesKey("theme")
|
||||||
val DARK_THEME = stringPreferencesKey("dark_theme")
|
val DARK_THEME = stringPreferencesKey("dark_theme")
|
||||||
|
|
@ -22,4 +23,9 @@ 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 TRANSLATOR_LANGUAGE_HISTORY = stringPreferencesKey("translator_languages_history")
|
||||||
}
|
}
|
||||||
|
|
@ -4,3 +4,6 @@ import android.graphics.Bitmap
|
||||||
|
|
||||||
typealias CoverImage = Bitmap
|
typealias CoverImage = Bitmap
|
||||||
typealias Selected = Boolean
|
typealias Selected = Boolean
|
||||||
|
typealias ID = Int
|
||||||
|
typealias LanguageCode = String
|
||||||
|
typealias Route = String
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
package ua.acclorite.book_story.domain.util
|
package ua.acclorite.book_story.domain.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Parcelable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
sealed class UIText(val string: String?) {
|
@Immutable
|
||||||
|
sealed class UIText(val string: String?) : Parcelable {
|
||||||
data class StringValue(val value: String) : UIText(value)
|
data class StringValue(val value: String) : UIText(value)
|
||||||
class StringResource(@StringRes val resId: Int, vararg val args: Any) : UIText(null)
|
class StringResource(@StringRes val resId: Int, vararg val args: Serializable) : UIText(null)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun asString(): String {
|
fun asString(): String {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package ua.acclorite.book_story.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.material3.BottomSheetDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Bottom Sheet. Fixes bugs with [ModalBottomSheet].
|
||||||
|
*
|
||||||
|
* @param modifier [Modifier].
|
||||||
|
* @param hasFixedHeight Whether the bottom sheet has fixed height passed through [modifier] or not.
|
||||||
|
* @param scrimColor Scrim color.
|
||||||
|
* @param shape Shape.
|
||||||
|
* @param containerColor Container color.
|
||||||
|
* @param onDismissRequest OnDismiss callback.
|
||||||
|
* @param dragHandle Drag Handle, pass null to disable.
|
||||||
|
* @param content Content inside [ModalBottomSheet]. Provides you navigation bar padding.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CustomBottomSheet(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
hasFixedHeight: Boolean = false,
|
||||||
|
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||||
|
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||||
|
content: @Composable ColumnScope.(navigationBarPadding: Dp) -> Unit
|
||||||
|
) {
|
||||||
|
val navigationBarPadding =
|
||||||
|
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
scrimColor = scrimColor,
|
||||||
|
dragHandle = dragHandle,
|
||||||
|
modifier = Modifier
|
||||||
|
.then(
|
||||||
|
if (hasFixedHeight) Modifier.align(Alignment.BottomCenter)
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(modifier),
|
||||||
|
onDismissRequest = {
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
shape = shape,
|
||||||
|
sheetState = rememberModalBottomSheetState(true),
|
||||||
|
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
||||||
|
containerColor = containerColor
|
||||||
|
) {
|
||||||
|
content(navigationBarPadding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,6 @@ import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -20,14 +19,21 @@ import ua.acclorite.book_story.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Checkbox. Has a Circle shape.
|
* Custom Checkbox. Has a Circle shape.
|
||||||
|
*
|
||||||
|
* @param selected Whether checked or not.
|
||||||
|
* @param containerColor Container color.
|
||||||
|
* @param size Size of the checkbox icon(not the whole checkbox).
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomCheckbox(selected: Boolean, containerColor: Color, size: Dp = 22.dp) {
|
fun CustomCheckbox(
|
||||||
|
selected: Boolean,
|
||||||
|
containerColor: Color,
|
||||||
|
size: Dp = 22.dp
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
tint = if (selected) containerColor else Color.Transparent,
|
tint = if (selected) containerColor else Color.Transparent,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(CircleShape)
|
|
||||||
.border(
|
.border(
|
||||||
width = if (!selected) 1.dp else 0.dp,
|
width = if (!selected) 1.dp else 0.dp,
|
||||||
if (!selected) MaterialTheme.colorScheme.outline else Color.Transparent,
|
if (!selected) MaterialTheme.colorScheme.outline else Color.Transparent,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
package ua.acclorite.book_story.presentation.components
|
package ua.acclorite.book_story.presentation.components
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
|
|
@ -7,6 +9,7 @@ import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
|
@ -15,6 +18,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalTextToolbar
|
import androidx.compose.ui.platform.LocalTextToolbar
|
||||||
|
|
@ -42,13 +46,13 @@ private class CustomTextActionModeCallback(
|
||||||
requireNotNull(menu)
|
requireNotNull(menu)
|
||||||
requireNotNull(mode)
|
requireNotNull(mode)
|
||||||
|
|
||||||
onTranslateRequested?.let {
|
onCopyRequested?.let {
|
||||||
menu.add(0, MENU_ITEM_TRANSLATE, 0, context.getString(R.string.translate))
|
menu.add(0, MENU_ITEM_COPY, 0, context.getString(R.string.copy))
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyRequested?.let {
|
onTranslateRequested?.let {
|
||||||
menu.add(0, MENU_ITEM_COPY, 1, context.getString(R.string.copy))
|
menu.add(0, MENU_ITEM_TRANSLATE, 1, context.getString(R.string.translate))
|
||||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +237,7 @@ fun CustomSelectionContainer(
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalTextToolbar provides customSelectionToolbar
|
LocalTextToolbar provides customSelectionToolbar
|
||||||
) {
|
) {
|
||||||
SelectionContainer {
|
SelectionContainer(Modifier.fillMaxSize()) {
|
||||||
content(isToolbarHidden.value)
|
content(isToolbarHidden.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.res.painterResource
|
import ua.acclorite.book_story.domain.util.Constants
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import ua.acclorite.book_story.R
|
|
||||||
import ua.acclorite.book_story.domain.model.NavigationItem
|
|
||||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
||||||
|
|
@ -31,41 +28,13 @@ fun BottomNavigationBar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
|
Constants.NAVIGATION_ITEMS.forEach {
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
item = NavigationItem(
|
item = it,
|
||||||
stringResource(id = R.string.library_screen),
|
isSelected = currentScreen == it.screen
|
||||||
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
|
|
||||||
unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
|
|
||||||
),
|
|
||||||
tooltipText = stringResource(id = R.string.library_content_desc),
|
|
||||||
isSelected = currentScreen == Screen.LIBRARY
|
|
||||||
) {
|
) {
|
||||||
navigator.navigate(Screen.LIBRARY, false)
|
navigator.navigate(it.screen, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
item = NavigationItem(
|
|
||||||
stringResource(id = R.string.history_screen),
|
|
||||||
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
|
|
||||||
unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
|
|
||||||
),
|
|
||||||
tooltipText = stringResource(id = R.string.history_content_desc),
|
|
||||||
isSelected = currentScreen == Screen.HISTORY
|
|
||||||
) {
|
|
||||||
navigator.navigate(Screen.HISTORY, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
BottomNavigationBarItem(
|
|
||||||
item = NavigationItem(
|
|
||||||
stringResource(id = R.string.browse_screen),
|
|
||||||
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
|
|
||||||
unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
|
|
||||||
),
|
|
||||||
tooltipText = stringResource(id = R.string.browse_content_desc),
|
|
||||||
isSelected = currentScreen == Screen.BROWSE
|
|
||||||
) {
|
|
||||||
navigator.navigate(Screen.BROWSE, false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.domain.model.NavigationItem
|
import ua.acclorite.book_story.domain.model.NavigationItem
|
||||||
|
|
@ -15,12 +17,16 @@ import ua.acclorite.book_story.presentation.components.CustomTooltip
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
|
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
|
||||||
|
*
|
||||||
|
* @param modifier Modifier.
|
||||||
|
* @param item [NavigationItem].
|
||||||
|
* @param isSelected Whether this screen is currently selected.
|
||||||
|
* @param onClick OnClick callback.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun RowScope.BottomNavigationBarItem(
|
fun RowScope.BottomNavigationBarItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: NavigationItem,
|
item: NavigationItem,
|
||||||
tooltipText: String,
|
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -33,11 +39,11 @@ fun RowScope.BottomNavigationBarItem(
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
label = {
|
label = {
|
||||||
CustomTooltip(
|
CustomTooltip(
|
||||||
text = tooltipText,
|
text = stringResource(id = item.tooltip),
|
||||||
padding = 64.dp
|
padding = 64.dp
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = stringResource(id = item.title),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
@ -47,11 +53,11 @@ fun RowScope.BottomNavigationBarItem(
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
icon = {
|
icon = {
|
||||||
CustomTooltip(
|
CustomTooltip(
|
||||||
text = tooltipText,
|
text = stringResource(id = item.tooltip),
|
||||||
padding = 32.dp
|
padding = 32.dp
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = icon,
|
painter = painterResource(id = icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package ua.acclorite.book_story.presentation.components.custom_dialog
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -57,7 +56,7 @@ import ua.acclorite.book_story.R
|
||||||
* @param withDivider Should divider be shown or not.
|
* @param withDivider Should divider be shown or not.
|
||||||
* @param items The items of this dialog, use items(..) { .. } or item { .. }.
|
* @param items The items of this dialog, use items(..) { .. } or item { .. }.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomDialogWithLazyColumn(
|
fun CustomDialogWithLazyColumn(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,8 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.domain.util.Constants
|
||||||
import ua.acclorite.book_story.domain.model.NavigationItem
|
|
||||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
||||||
|
|
@ -69,38 +66,13 @@ fun BoxScope.CustomNavigationRail() {
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
Constants.NAVIGATION_ITEMS.forEach {
|
||||||
CustomNavigationRailItem(
|
CustomNavigationRailItem(
|
||||||
item = NavigationItem(
|
item = it,
|
||||||
stringResource(id = R.string.library_screen),
|
isSelected = currentScreen == it.screen
|
||||||
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
|
|
||||||
unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
|
|
||||||
),
|
|
||||||
tooltipText = stringResource(id = R.string.library_content_desc),
|
|
||||||
isSelected = currentScreen == Screen.LIBRARY
|
|
||||||
) {
|
) {
|
||||||
navigator.navigate(Screen.LIBRARY, false)
|
navigator.navigate(it.screen, false)
|
||||||
}
|
}
|
||||||
CustomNavigationRailItem(
|
|
||||||
item = NavigationItem(
|
|
||||||
stringResource(id = R.string.history_screen),
|
|
||||||
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
|
|
||||||
unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
|
|
||||||
),
|
|
||||||
tooltipText = stringResource(id = R.string.history_content_desc),
|
|
||||||
isSelected = currentScreen == Screen.HISTORY
|
|
||||||
) {
|
|
||||||
navigator.navigate(Screen.HISTORY, false)
|
|
||||||
}
|
|
||||||
CustomNavigationRailItem(
|
|
||||||
item = NavigationItem(
|
|
||||||
stringResource(id = R.string.browse_screen),
|
|
||||||
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
|
|
||||||
unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
|
|
||||||
),
|
|
||||||
tooltipText = stringResource(id = R.string.browse_content_desc),
|
|
||||||
isSelected = currentScreen == Screen.BROWSE
|
|
||||||
) {
|
|
||||||
navigator.navigate(Screen.BROWSE, false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.domain.model.NavigationItem
|
import ua.acclorite.book_story.domain.model.NavigationItem
|
||||||
|
|
@ -14,12 +16,16 @@ import ua.acclorite.book_story.presentation.components.CustomTooltip
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Navigation Rail item.
|
* Custom Navigation Rail item.
|
||||||
|
*
|
||||||
|
* @param modifier Modifier.
|
||||||
|
* @param item [NavigationItem].
|
||||||
|
* @param isSelected Whether this screen is currently selected.
|
||||||
|
* @param onClick OnClick callback.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomNavigationRailItem(
|
fun CustomNavigationRailItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: NavigationItem,
|
item: NavigationItem,
|
||||||
tooltipText: String,
|
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -31,11 +37,11 @@ fun CustomNavigationRailItem(
|
||||||
NavigationRailItem(
|
NavigationRailItem(
|
||||||
label = {
|
label = {
|
||||||
CustomTooltip(
|
CustomTooltip(
|
||||||
text = tooltipText,
|
text = stringResource(id = item.tooltip),
|
||||||
padding = 48.dp
|
padding = 48.dp
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = item.title,
|
text = stringResource(id = item.title),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|
@ -45,11 +51,11 @@ fun CustomNavigationRailItem(
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
icon = {
|
icon = {
|
||||||
CustomTooltip(
|
CustomTooltip(
|
||||||
text = tooltipText,
|
text = stringResource(id = item.tooltip),
|
||||||
padding = 16.dp
|
padding = 16.dp
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = icon,
|
painter = painterResource(id = icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
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.domain.model.LanguageHistory
|
||||||
|
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.data.MainEvent
|
||||||
|
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||||
|
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 mainViewModel: MainViewModel = hiltViewModel()
|
||||||
|
|
||||||
|
val state = translatorLanguageViewModel.state.collectAsState()
|
||||||
|
val mainState = mainViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
val loading = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
translatorLanguageViewModel.init(
|
||||||
|
selectedLanguage = selectedLanguage,
|
||||||
|
unselectedLanguage = unselectedLanguage,
|
||||||
|
translateFromSelecting = translateFromSelecting,
|
||||||
|
history = mainState.value.translatorLanguageHistory!!,
|
||||||
|
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") {
|
||||||
|
val currentHistory = mainState.value.translatorLanguageHistory!!.toMutableList()
|
||||||
|
val langToAdd = state.value.languageToSelect
|
||||||
|
val newHistory = mutableListOf<LanguageHistory>()
|
||||||
|
val indexOfItem = currentHistory.indexOfFirst { it.languageCode == langToAdd }
|
||||||
|
|
||||||
|
if (indexOfItem != -1) {
|
||||||
|
currentHistory.removeAt(indexOfItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHistory.add(0, LanguageHistory(0, langToAdd))
|
||||||
|
|
||||||
|
currentHistory.sortedBy { it.id }.forEachIndexed { index, languageHistory ->
|
||||||
|
newHistory.add(LanguageHistory(index, languageHistory.languageCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
mainViewModel.onEvent(
|
||||||
|
MainEvent.OnChangeTranslatorLanguageHistory(
|
||||||
|
newHistory
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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 = Color.Transparent,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
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.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()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,424 @@
|
||||||
|
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.IsLanguageModelDownloaded
|
||||||
|
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
|
||||||
|
) : 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(
|
||||||
|
selectedLanguage: String,
|
||||||
|
unselectedLanguage: String,
|
||||||
|
translateFromSelecting: Boolean,
|
||||||
|
history: List<LanguageHistory>,
|
||||||
|
loaded: () -> Unit
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val oldState = _state.value
|
||||||
|
_state.update {
|
||||||
|
TranslatorLanguageState()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}?.id
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,3 +10,61 @@ 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()
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package ua.acclorite.book_story.presentation.data
|
package ua.acclorite.book_story.presentation.data
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||||
|
|
||||||
sealed class MainEvent {
|
sealed class MainEvent {
|
||||||
data class OnChangeLanguage(val lang: String) : MainEvent()
|
data class OnChangeLanguage(val lang: String) : MainEvent()
|
||||||
data class OnChangeTheme(val theme: String) : MainEvent()
|
data class OnChangeTheme(val theme: String) : MainEvent()
|
||||||
|
|
@ -17,4 +19,9 @@ 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 OnChangeTranslatorLanguageHistory(val history: List<LanguageHistory>) : MainEvent()
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,12 @@ import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
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 com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.Types
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||||
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
|
||||||
import ua.acclorite.book_story.presentation.ui.DarkTheme
|
import ua.acclorite.book_story.presentation.ui.DarkTheme
|
||||||
|
|
@ -17,8 +23,19 @@ import ua.acclorite.book_story.presentation.ui.toTheme
|
||||||
import ua.acclorite.book_story.presentation.ui.toThemeContrast
|
import ua.acclorite.book_story.presentation.ui.toThemeContrast
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
val translatorHistoryMoshi: JsonAdapter<List<LanguageHistory>> = Moshi
|
||||||
|
.Builder()
|
||||||
|
.add(KotlinJsonAdapterFactory())
|
||||||
|
.build()
|
||||||
|
.adapter(
|
||||||
|
Types.newParameterizedType(
|
||||||
|
List::class.java,
|
||||||
|
LanguageHistory::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset.
|
* Main State. All app's settings/preferences/permanent-variables are here. Wrapped in SavedStateHandle, so it won't reset.
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
data class MainState(
|
data class MainState(
|
||||||
|
|
@ -38,7 +55,13 @@ 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 translatorLanguageHistory: List<LanguageHistory>? = null,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
// String
|
// String
|
||||||
language = parcel.readString(),
|
language = parcel.readString(),
|
||||||
|
|
@ -75,6 +98,19 @@ data class MainState(
|
||||||
.toInt() == 1 else null,
|
.toInt() == 1 else null,
|
||||||
// Int
|
// Int
|
||||||
sidePadding = if (parcel.readByte().toInt() != 0) parcel.readInt() else null,
|
sidePadding = if (parcel.readByte().toInt() != 0) parcel.readInt() else null,
|
||||||
|
// Boolean
|
||||||
|
enableTranslator = if (parcel.readByte().toInt() != 0) parcel.readByte()
|
||||||
|
.toInt() == 1 else null,
|
||||||
|
// String
|
||||||
|
translateFrom = parcel.readString(),
|
||||||
|
// String
|
||||||
|
translateTo = parcel.readString(),
|
||||||
|
// Boolean
|
||||||
|
doubleClickTranslation = if (parcel.readByte().toInt() != 0) parcel.readByte()
|
||||||
|
.toInt() == 1 else null,
|
||||||
|
translatorLanguageHistory = parcel.readString()?.let {
|
||||||
|
translatorHistoryMoshi.fromJson(it)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
|
@ -117,9 +153,21 @@ data class MainState(
|
||||||
// Check For Updates
|
// Check For Updates
|
||||||
parcel.writeByte((if (checkForUpdates != null) 1 else 0).toByte())
|
parcel.writeByte((if (checkForUpdates != null) 1 else 0).toByte())
|
||||||
checkForUpdates?.let { parcel.writeByte(if (it) 1 else 0) }
|
checkForUpdates?.let { parcel.writeByte(if (it) 1 else 0) }
|
||||||
// Side padding
|
// Side Padding
|
||||||
parcel.writeByte((if (sidePadding != null) 1 else 0).toByte())
|
parcel.writeByte((if (sidePadding != null) 1 else 0).toByte())
|
||||||
sidePadding?.let { parcel.writeInt(it) }
|
sidePadding?.let { parcel.writeInt(it) }
|
||||||
|
// Enable Translator
|
||||||
|
parcel.writeByte((if (enableTranslator != null) 1 else 0).toByte())
|
||||||
|
enableTranslator?.let { parcel.writeByte(if (it) 1 else 0) }
|
||||||
|
// Translate From
|
||||||
|
parcel.writeString(translateFrom)
|
||||||
|
// Translate To
|
||||||
|
parcel.writeString(translateTo)
|
||||||
|
// Double Click Translation
|
||||||
|
parcel.writeByte((if (doubleClickTranslation != null) 1 else 0).toByte())
|
||||||
|
doubleClickTranslation?.let { parcel.writeByte(if (it) 1 else 0) }
|
||||||
|
// Translator Language History
|
||||||
|
parcel.writeString(translatorHistoryMoshi.toJson(translatorLanguageHistory))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
|
|
@ -140,7 +188,6 @@ data class MainState(
|
||||||
*/
|
*/
|
||||||
fun initialize(data: Map<String, Any>): MainState {
|
fun initialize(data: Map<String, Any>): MainState {
|
||||||
DataStoreConstants.apply {
|
DataStoreConstants.apply {
|
||||||
|
|
||||||
val language: String = data[LANGUAGE.name] as? String ?: if (
|
val language: String = data[LANGUAGE.name] as? String ?: if (
|
||||||
Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }
|
Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }
|
||||||
) {
|
) {
|
||||||
|
|
@ -195,6 +242,30 @@ 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 =
|
||||||
|
data[DOUBLE_CLICK_TRANSLATION.name] as? Boolean
|
||||||
|
?: true
|
||||||
|
|
||||||
|
val translatorLanguageHistory: List<LanguageHistory> =
|
||||||
|
(data[TRANSLATOR_LANGUAGE_HISTORY.name] as? String)?.let {
|
||||||
|
translatorHistoryMoshi.fromJson(it)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
|
||||||
return MainState(
|
return MainState(
|
||||||
language = language,
|
language = language,
|
||||||
theme = theme.toTheme(),
|
theme = theme.toTheme(),
|
||||||
|
|
@ -211,7 +282,12 @@ data class MainState(
|
||||||
paragraphHeight = paragraphHeight,
|
paragraphHeight = paragraphHeight,
|
||||||
paragraphIndentation = paragraphIndentation,
|
paragraphIndentation = paragraphIndentation,
|
||||||
checkForUpdates = checkForUpdates,
|
checkForUpdates = checkForUpdates,
|
||||||
sidePadding = sidePadding
|
sidePadding = sidePadding,
|
||||||
|
enableTranslator = enableTranslator,
|
||||||
|
translateFrom = translateFrom,
|
||||||
|
translateTo = translateTo,
|
||||||
|
doubleClickTranslation = doubleClickTranslation,
|
||||||
|
translatorLanguageHistory = translatorLanguageHistory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,66 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is MainEvent.OnChangeEnableTranslator -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
setDatastore.execute(DataStoreConstants.ENABLE_TRANSLATOR, event.bool)
|
||||||
|
updateStateWithSavedHandle {
|
||||||
|
it.copy(
|
||||||
|
enableTranslator = event.bool
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainEvent.OnChangeTranslateFrom -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
setDatastore.execute(DataStoreConstants.TRANSLATE_FROM, event.languageCode)
|
||||||
|
updateStateWithSavedHandle {
|
||||||
|
it.copy(
|
||||||
|
translateFrom = event.languageCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainEvent.OnChangeTranslateTo -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
setDatastore.execute(DataStoreConstants.TRANSLATE_TO, event.languageCode)
|
||||||
|
updateStateWithSavedHandle {
|
||||||
|
it.copy(
|
||||||
|
translateTo = event.languageCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainEvent.OnChangeDoubleClickTranslation -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
setDatastore.execute(DataStoreConstants.DOUBLE_CLICK_TRANSLATION, event.bool)
|
||||||
|
updateStateWithSavedHandle {
|
||||||
|
it.copy(
|
||||||
|
doubleClickTranslation = event.bool
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainEvent.OnChangeTranslatorLanguageHistory -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val historyJson = translatorHistoryMoshi.toJson(event.history)
|
||||||
|
|
||||||
|
setDatastore.execute(
|
||||||
|
DataStoreConstants.TRANSLATOR_LANGUAGE_HISTORY,
|
||||||
|
historyJson
|
||||||
|
)
|
||||||
|
updateStateWithSavedHandle {
|
||||||
|
it.copy(
|
||||||
|
translatorLanguageHistory = event.history
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.about.data
|
package ua.acclorite.book_story.presentation.screens.about.data
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class AboutState(
|
data class AboutState(
|
||||||
val showUpdateDialog: Boolean = false,
|
val showUpdateDialog: Boolean = false,
|
||||||
val updateInfo: ReleaseResponse? = null
|
val updateInfo: LatestReleaseInfo? = null
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.LargeTopAppBar
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -24,6 +23,7 @@ import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.util.Constants
|
import ua.acclorite.book_story.domain.util.Constants
|
||||||
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.customItems
|
||||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
|
import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
|
||||||
|
|
@ -78,7 +78,7 @@ private fun CreditsScreen(
|
||||||
.padding(top = paddingValues.calculateTopPadding()),
|
.padding(top = paddingValues.calculateTopPadding()),
|
||||||
state = scrollState.second
|
state = scrollState.second
|
||||||
) {
|
) {
|
||||||
items(Constants.CREDITS, key = { it.name }) {
|
customItems(Constants.CREDITS, key = { it.name }) {
|
||||||
CreditItem(credit = it) {
|
CreditItem(credit = it) {
|
||||||
onAboutNavigateEvent(
|
onAboutNavigateEvent(
|
||||||
AboutEvent.OnNavigateToBrowserPage(
|
AboutEvent.OnNavigateToBrowserPage(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,11 @@ import ua.acclorite.book_story.domain.model.Credit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credit Item.
|
* Credit Item.
|
||||||
|
*
|
||||||
|
* @param credit [Credit] item.
|
||||||
|
* @param horizontalPadding Horizontal item padding.
|
||||||
|
* @param verticalPadding Vertical item padding.
|
||||||
|
* @param onClick OnClick callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -67,7 +72,7 @@ fun CreditItem(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(100))
|
.clip(RoundedCornerShape(100))
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(100))
|
||||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,10 @@ private fun LicenseInfoScreen(
|
||||||
.clickable(enabled = license.licenseContent?.isNotBlank() == true) {
|
.clickable(enabled = license.licenseContent?.isNotBlank() == true) {
|
||||||
showed.value = !showed.value
|
showed.value = !showed.value
|
||||||
}
|
}
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
.background(
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
RoundedCornerShape(100)
|
||||||
|
)
|
||||||
.padding(vertical = 6.dp, horizontal = 12.dp)
|
.padding(vertical = 6.dp, horizontal = 12.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ import com.mikepenz.aboutlibraries.ui.compose.m3.util.author
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* License Item.
|
* License Item.
|
||||||
|
*
|
||||||
|
* @param modifier Modifier.
|
||||||
|
* @param library [Library] item.
|
||||||
|
* @param horizontalPadding Horizontal item padding.
|
||||||
|
* @param verticalPadding Vertical item padding.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -85,7 +90,7 @@ fun LicenseItem(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(100))
|
.clip(RoundedCornerShape(100))
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(100))
|
||||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,10 @@ import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Info section.
|
* BookInfo's Info section.
|
||||||
|
*
|
||||||
|
* @param state [BookInfoState] instance.
|
||||||
|
* @param onEvent [BookInfoEvent] callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -65,7 +68,10 @@ fun BookInfoInfoSection(
|
||||||
.height(195.dp)
|
.height(195.dp)
|
||||||
.width(130.dp)
|
.width(130.dp)
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = null,
|
indication = null,
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,11 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.HideImage
|
import androidx.compose.material.icons.filled.HideImage
|
||||||
import androidx.compose.material.icons.filled.ImageSearch
|
import androidx.compose.material.icons.filled.ImageSearch
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -24,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomBottomSheet
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
|
||||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
|
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
|
||||||
|
|
@ -31,8 +25,12 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
|
* Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
|
||||||
|
*
|
||||||
|
* @param state [BookInfoState] instance.
|
||||||
|
* @param onEvent [BookInfoEvent] callback.
|
||||||
|
* @param onLibraryUpdateEvent [LibraryEvent] callback.
|
||||||
|
* @param onHistoryUpdateEvent [HistoryEvent] callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoChangeCoverBottomSheet(
|
fun BookInfoChangeCoverBottomSheet(
|
||||||
state: State<BookInfoState>,
|
state: State<BookInfoState>,
|
||||||
|
|
@ -41,8 +39,6 @@ fun BookInfoChangeCoverBottomSheet(
|
||||||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
|
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigationBarPadding =
|
|
||||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
|
||||||
val photoPicker = rememberLauncherForActivityResult(
|
val photoPicker = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.PickVisualMedia(),
|
contract = ActivityResultContracts.PickVisualMedia(),
|
||||||
onResult = { uri ->
|
onResult = { uri ->
|
||||||
|
|
@ -66,16 +62,12 @@ fun BookInfoChangeCoverBottomSheet(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ModalBottomSheet(
|
CustomBottomSheet(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
|
onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
|
||||||
},
|
}
|
||||||
sheetState = rememberModalBottomSheetState(true),
|
) { padding ->
|
||||||
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
|
||||||
) {
|
|
||||||
|
|
||||||
BookInfoChangeCoverBottomSheetItem(
|
BookInfoChangeCoverBottomSheetItem(
|
||||||
icon = Icons.Default.ImageSearch,
|
icon = Icons.Default.ImageSearch,
|
||||||
text = stringResource(id = R.string.change_cover),
|
text = stringResource(id = R.string.change_cover),
|
||||||
|
|
@ -110,7 +102,7 @@ fun BookInfoChangeCoverBottomSheet(
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier.height(
|
modifier = Modifier.height(
|
||||||
8.dp + navigationBarPadding
|
8.dp + padding
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,8 @@ package ua.acclorite.book_story.presentation.screens.book_info.components.detail
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -19,6 +12,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomBottomSheet
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoState
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -28,16 +22,16 @@ import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Details bottom sheet. Displays name, path, last opened and size of the file.
|
* Details bottom sheet. Displays name, path, last opened and size of the file.
|
||||||
|
*
|
||||||
|
* @param state [BookInfoState] instance.
|
||||||
|
* @param onEvent [BookInfoEvent] callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoDetailsBottomSheet(
|
fun BookInfoDetailsBottomSheet(
|
||||||
state: State<BookInfoState>,
|
state: State<BookInfoState>,
|
||||||
onEvent: (BookInfoEvent) -> Unit
|
onEvent: (BookInfoEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigationBarPadding =
|
|
||||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
|
||||||
|
|
||||||
val pattern = remember {
|
val pattern = remember {
|
||||||
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
|
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
|
||||||
|
|
@ -66,14 +60,11 @@ fun BookInfoDetailsBottomSheet(
|
||||||
else ""
|
else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
ModalBottomSheet(
|
CustomBottomSheet(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
|
onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
|
||||||
},
|
}
|
||||||
sheetState = rememberModalBottomSheetState(true),
|
|
||||||
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
|
||||||
) {
|
) {
|
||||||
BookInfoDetailsBottomSheetItem(
|
BookInfoDetailsBottomSheetItem(
|
||||||
title = stringResource(id = R.string.file_name),
|
title = stringResource(id = R.string.file_name),
|
||||||
|
|
@ -151,7 +142,7 @@ fun BookInfoDetailsBottomSheet(
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier.height(
|
modifier = Modifier.height(
|
||||||
8.dp + navigationBarPadding
|
8.dp + it
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
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.StringWithId
|
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -58,7 +57,7 @@ sealed class BookInfoEvent {
|
||||||
data object OnDismissConfirmUpdateDialog : BookInfoEvent()
|
data object OnDismissConfirmUpdateDialog : BookInfoEvent()
|
||||||
|
|
||||||
data class OnShowConfirmUpdateDialog(
|
data class OnShowConfirmUpdateDialog(
|
||||||
val updatedBook: Pair<Book, List<StringWithId>>,
|
val updatedBook: Pair<Book, List<String>>,
|
||||||
val authorUpdated: Boolean,
|
val authorUpdated: Boolean,
|
||||||
val descriptionUpdated: Boolean,
|
val descriptionUpdated: Boolean,
|
||||||
val textUpdated: Boolean,
|
val textUpdated: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
|
||||||
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.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
import ua.acclorite.book_story.domain.util.Constants
|
import ua.acclorite.book_story.domain.util.Constants
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -13,7 +12,7 @@ data class BookInfoState(
|
||||||
val isLoadingUpdate: Boolean = false,
|
val isLoadingUpdate: Boolean = false,
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val showConfirmUpdateDialog: Boolean = false,
|
val showConfirmUpdateDialog: Boolean = false,
|
||||||
val updatedBook: Pair<Book, List<StringWithId>>? = null,
|
val updatedBook: Pair<Book, List<String>>? = null,
|
||||||
val authorChanged: Boolean = false,
|
val authorChanged: Boolean = false,
|
||||||
val descriptionChanged: Boolean = false,
|
val descriptionChanged: Boolean = false,
|
||||||
val textChanged: Boolean = false,
|
val textChanged: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -386,7 +386,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.map { it.line } != text.map { it.line }) {
|
if (updatedText != text.map { it.value.originalLine }) {
|
||||||
textUpdated = true
|
textUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -41,6 +42,12 @@ import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse list element item. Can be selected.
|
* Browse list element item. Can be selected.
|
||||||
|
*
|
||||||
|
* @param file [File].
|
||||||
|
* @param hasSelectedFiles Whether parent list has selected items.
|
||||||
|
* @param modifier Modifier.
|
||||||
|
* @param onClick OnClick callback.
|
||||||
|
* @param onLongClick OnLongClick callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -52,12 +59,12 @@ fun BrowseFileItem(
|
||||||
onLongClick: () -> Unit
|
onLongClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
|
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
|
||||||
val lastModified = remember { pattern.format(Date(file.first.lastModified())) }
|
val lastModified = rememberSaveable { pattern.format(Date(file.first.lastModified())) }
|
||||||
val sizeBytes = remember { file.first.length() }
|
val sizeBytes = rememberSaveable { file.first.length() }
|
||||||
|
|
||||||
val fileSizeKB = remember { if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 }
|
val fileSizeKB = rememberSaveable { if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 }
|
||||||
val fileSizeMB = remember { if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 }
|
val fileSizeMB = rememberSaveable { if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 }
|
||||||
val fileSize = remember {
|
val fileSize = rememberSaveable {
|
||||||
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
||||||
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
||||||
else "0 KB"
|
else "0 KB"
|
||||||
|
|
@ -74,7 +81,7 @@ fun BrowseFileItem(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 3.dp)
|
.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.background(backgroundColor)
|
.background(backgroundColor, RoundedCornerShape(10.dp))
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
onLongClick()
|
onLongClick()
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,14 @@ import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* History list element item.
|
* History list element item.
|
||||||
|
*
|
||||||
|
* @param modifier Modifier.
|
||||||
|
* @param history [History] item.
|
||||||
|
* @param isOnClickEnabled Whether OnClick is enabled.
|
||||||
|
* @param onBodyClick OnBodyClick callback.
|
||||||
|
* @param onTitleClick OnTitleClick callback.
|
||||||
|
* @param isDeleteEnabled Whether Delete button is enabled.
|
||||||
|
* @param onDeleteClick OnDeleteClick callback.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryItem(
|
fun HistoryItem(
|
||||||
|
|
@ -73,8 +81,10 @@ fun HistoryItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(90.dp)
|
.height(90.dp)
|
||||||
.width(60.dp)
|
.width(60.dp)
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.background(
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
RoundedCornerShape(10.dp)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (history.book?.coverImage != null) {
|
if (history.book?.coverImage != null) {
|
||||||
CustomCoverImage(
|
CustomCoverImage(
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
|
@ -179,8 +178,10 @@ private fun LibraryScreen(
|
||||||
Text(
|
Text(
|
||||||
text = state.value.books.size.toString(),
|
text = state.value.books.size.toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(14.dp))
|
.background(
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontSize = 16.sp
|
fontSize = 16.sp
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,12 @@ import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Library list element item.
|
* Library list element item.
|
||||||
|
*
|
||||||
|
* @param book [Pair] of [Book] and [Selected] (Boolean).
|
||||||
|
* @param modifier Modifier.
|
||||||
|
* @param onCoverImageClick OnCoverImageClick callback.
|
||||||
|
* @param onLongClick OnLongClick callback.
|
||||||
|
* @param onButtonClick OnButtonClick callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -86,7 +92,10 @@ fun LibraryBookItem(
|
||||||
.aspectRatio(1f / 1.5f)
|
.aspectRatio(1f / 1.5f)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
onCoverImageClick()
|
onCoverImageClick()
|
||||||
|
|
@ -124,7 +133,7 @@ fun LibraryBookItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(6.dp)
|
.padding(6.dp)
|
||||||
.clip(MaterialTheme.shapes.small)
|
.clip(MaterialTheme.shapes.small)
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
.background(MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -113,8 +112,10 @@ fun LibraryTabRow(
|
||||||
Text(
|
Text(
|
||||||
text = tabItem.second.toString(),
|
text = tabItem.second.toString(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.background(
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.selection.DisableSelection
|
import androidx.compose.foundation.text.selection.DisableSelection
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -32,7 +29,6 @@ import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
@ -42,24 +38,21 @@ 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.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.LineBreak
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.util.Constants
|
import ua.acclorite.book_story.domain.util.Constants
|
||||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
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.customItemsIndexed
|
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
|
||||||
|
|
@ -72,8 +65,11 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
|
||||||
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.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
|
||||||
|
|
@ -88,12 +84,27 @@ fun ReaderScreenRoot() {
|
||||||
val libraryViewModel: LibraryViewModel = hiltViewModel()
|
val libraryViewModel: LibraryViewModel = hiltViewModel()
|
||||||
val historyViewModel: HistoryViewModel = hiltViewModel()
|
val historyViewModel: HistoryViewModel = hiltViewModel()
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val loading = remember { mutableStateOf(true) }
|
val loading = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
val state = viewModel.state.collectAsState()
|
val state = viewModel.state.collectAsState()
|
||||||
val mainState = mainViewModel.state.collectAsState()
|
val mainState = mainViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
val canScroll by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
state.value.listState.canScrollBackward && state.value.listState.canScrollForward
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val firstVisibleItemIndex = remember {
|
||||||
|
derivedStateOf {
|
||||||
|
state.value.listState.firstVisibleItemIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val firstVisibleItemOffset = remember {
|
||||||
|
derivedStateOf {
|
||||||
|
state.value.listState.firstVisibleItemScrollOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(
|
viewModel.init(
|
||||||
navigator,
|
navigator,
|
||||||
|
|
@ -102,17 +113,37 @@ fun ReaderScreenRoot() {
|
||||||
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
|
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
|
||||||
historyViewModel.onEvent(HistoryEvent.OnLoadList)
|
historyViewModel.onEvent(HistoryEvent.OnLoadList)
|
||||||
},
|
},
|
||||||
listState = listState,
|
onError = {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
it.asString(context),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
},
|
||||||
onLoaded = {
|
onLoaded = {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(canScroll) {
|
||||||
|
if (!canScroll && !state.value.showMenu && !loading.value) {
|
||||||
|
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(firstVisibleItemIndex.value, firstVisibleItemOffset.value) {
|
||||||
|
viewModel.onUpdateProgress(
|
||||||
|
firstVisibleItemIndex = firstVisibleItemIndex.value,
|
||||||
|
firstVisibleItemOffset = firstVisibleItemOffset.value,
|
||||||
|
navigator = navigator,
|
||||||
|
isReaderLoading = loading.value,
|
||||||
|
onLibraryEvent = libraryViewModel::onEvent,
|
||||||
|
onHistoryEvent = historyViewModel::onEvent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ReaderScreen(
|
ReaderScreen(
|
||||||
state = state,
|
state = state,
|
||||||
mainState = mainState,
|
mainState = mainState,
|
||||||
listState = listState,
|
|
||||||
loading = loading,
|
loading = loading,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
onEvent = viewModel::onEvent,
|
onEvent = viewModel::onEvent,
|
||||||
|
|
@ -122,13 +153,11 @@ fun ReaderScreenRoot() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReaderScreen(
|
private fun ReaderScreen(
|
||||||
state: State<ReaderState>,
|
state: State<ReaderState>,
|
||||||
mainState: State<MainState>,
|
mainState: State<MainState>,
|
||||||
listState: LazyListState,
|
|
||||||
loading: State<Boolean>,
|
loading: State<Boolean>,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
onEvent: (ReaderEvent) -> Unit,
|
onEvent: (ReaderEvent) -> Unit,
|
||||||
|
|
@ -137,13 +166,9 @@ private fun ReaderScreen(
|
||||||
onHistoryEvent: (HistoryEvent) -> Unit,
|
onHistoryEvent: (HistoryEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current as ComponentActivity
|
val context = LocalContext.current as ComponentActivity
|
||||||
val systemBarsColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy(0.85f)
|
val density = LocalDensity.current
|
||||||
|
val systemBarsColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy(0.94f)
|
||||||
|
|
||||||
val canScroll by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
listState.canScrollBackward && listState.canScrollForward
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val nestedScrollConnection = remember(state) {
|
val nestedScrollConnection = remember(state) {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
override fun onPostScroll(
|
override fun onPostScroll(
|
||||||
|
|
@ -162,6 +187,9 @@ private fun ReaderScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val text = remember(state.value.text) {
|
||||||
|
state.value.text.toList().sortedBy { it.first }
|
||||||
|
}
|
||||||
val fontFamily = remember(mainState.value.fontFamily) {
|
val fontFamily = remember(mainState.value.fontFamily) {
|
||||||
Constants.FONTS.find {
|
Constants.FONTS.find {
|
||||||
it.id == mainState.value.fontFamily
|
it.id == mainState.value.fontFamily
|
||||||
|
|
@ -190,46 +218,45 @@ private fun ReaderScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.value.showTranslatorBottomSheet) {
|
||||||
LaunchedEffect(canScroll) {
|
ReaderTranslatorBottomSheet(
|
||||||
if (!canScroll && !state.value.showMenu && !loading.value) {
|
state = state,
|
||||||
onEvent(ReaderEvent.OnShowHideMenu(context = context))
|
onEvent = onEvent
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
if (state.value.showLanguageBottomSheet) {
|
||||||
|
TranslatorLanguageBottomSheet(
|
||||||
|
selectedLanguage = if (state.value.languageBottomSheetTranslateFrom) {
|
||||||
|
state.value.book.translateFrom
|
||||||
|
} else state.value.book.translateTo,
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
unselectedLanguage = if (!state.value.languageBottomSheetTranslateFrom) {
|
||||||
snapshotFlow {
|
state.value.book.translateFrom
|
||||||
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
|
} else state.value.book.translateTo,
|
||||||
}.debounce(500).collectLatest { items ->
|
|
||||||
if (!loading.value && state.value.text.isNotEmpty() && listState.layoutInfo.totalItemsCount > 0) {
|
|
||||||
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
|
|
||||||
|
|
||||||
val progress = if (items.first > 0) {
|
|
||||||
if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) {
|
|
||||||
1f
|
|
||||||
} else {
|
|
||||||
(items.first.toFloat() / (state.value.text.lastIndex).toFloat())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0f
|
|
||||||
}
|
|
||||||
|
|
||||||
|
translateFromSelecting = state.value.languageBottomSheetTranslateFrom,
|
||||||
|
onSelect = { from, to ->
|
||||||
onEvent(
|
onEvent(
|
||||||
ReaderEvent.OnChangeProgress(
|
ReaderEvent.OnChangeTranslatorSettings(
|
||||||
progress = progress,
|
translateFrom = from,
|
||||||
navigator = navigator,
|
translateTo = to
|
||||||
firstVisibleItemIndex = items.first,
|
)
|
||||||
firstVisibleItemOffset = items.second,
|
)
|
||||||
refreshList = { book ->
|
onEvent(
|
||||||
onLibraryEvent(LibraryEvent.OnUpdateBook(book))
|
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||||
onHistoryEvent(HistoryEvent.OnUpdateBook(book))
|
show = false
|
||||||
}
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
onEvent(
|
||||||
|
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||||
|
show = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (state.value.showSettingsBottomSheet) {
|
if (state.value.showSettingsBottomSheet) {
|
||||||
ReaderSettingsBottomSheet(
|
ReaderSettingsBottomSheet(
|
||||||
mainState = mainState,
|
mainState = mainState,
|
||||||
|
|
@ -237,6 +264,12 @@ private fun ReaderScreen(
|
||||||
onMainEvent = onMainEvent
|
onMainEvent = onMainEvent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (state.value.showDownloadLanguageDialog) {
|
||||||
|
ReaderDownloadLanguageDialog(
|
||||||
|
state = state,
|
||||||
|
onEvent = onEvent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -254,13 +287,13 @@ private fun ReaderScreen(
|
||||||
onEvent = onEvent,
|
onEvent = onEvent,
|
||||||
onLibraryUpdateEvent = onLibraryEvent,
|
onLibraryUpdateEvent = onLibraryEvent,
|
||||||
onHistoryUpdateEvent = onHistoryEvent,
|
onHistoryUpdateEvent = onHistoryEvent,
|
||||||
listState = listState,
|
|
||||||
containerColor = systemBarsColor
|
containerColor = systemBarsColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
CustomAnimatedVisibility(
|
CustomAnimatedVisibility(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
visible = state.value.showMenu,
|
visible = state.value.showMenu,
|
||||||
enter = slideInVertically { it },
|
enter = slideInVertically { it },
|
||||||
exit = slideOutVertically { it }
|
exit = slideOutVertically { it }
|
||||||
|
|
@ -270,7 +303,6 @@ private fun ReaderScreen(
|
||||||
onEvent = onEvent,
|
onEvent = onEvent,
|
||||||
onLibraryUpdateEvent = onLibraryEvent,
|
onLibraryUpdateEvent = onLibraryEvent,
|
||||||
onHistoryUpdateEvent = onHistoryEvent,
|
onHistoryUpdateEvent = onHistoryEvent,
|
||||||
listState = listState,
|
|
||||||
containerColor = systemBarsColor
|
containerColor = systemBarsColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -288,7 +320,7 @@ private fun ReaderScreen(
|
||||||
},
|
},
|
||||||
onTranslateRequested = { textToTranslate ->
|
onTranslateRequested = { textToTranslate ->
|
||||||
onEvent(
|
onEvent(
|
||||||
ReaderEvent.OnTranslateText(
|
ReaderEvent.OnOpenTranslator(
|
||||||
textToTranslate,
|
textToTranslate,
|
||||||
context,
|
context,
|
||||||
noAppsFound = {
|
noAppsFound = {
|
||||||
|
|
@ -318,9 +350,10 @@ private fun ReaderScreen(
|
||||||
}
|
}
|
||||||
) { toolbarShowed ->
|
) { toolbarShowed ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = state.value.listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
.background(backgroundColor)
|
||||||
.then(
|
.then(
|
||||||
if (!loading.value && toolbarShowed) {
|
if (!loading.value && toolbarShowed) {
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -336,56 +369,49 @@ private fun ReaderScreen(
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(paragraphHeight)
|
||||||
) {
|
) {
|
||||||
if (state.value.text.isNotEmpty()) {
|
if (state.value.text.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
DisableSelection {
|
DisableSelection {
|
||||||
ReaderStartItem(state = state)
|
ReaderStartItem(state = state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customItemsIndexed(
|
customItems(
|
||||||
state.value.text, key = { key -> key.id }
|
text, key = { key -> key.first }
|
||||||
) { index, line ->
|
) { line ->
|
||||||
val text = remember(mainState.value.paragraphIndentation, line) {
|
ReaderTextParagraph(
|
||||||
"${if (mainState.value.paragraphIndentation!!) " " else ""}${line.line}"
|
state = state,
|
||||||
}
|
id = line.first,
|
||||||
|
line = line.second,
|
||||||
Column(
|
context = context,
|
||||||
Modifier
|
density = density,
|
||||||
.background(backgroundColor)
|
fontFamily = fontFamily,
|
||||||
.fillMaxWidth()
|
fontColor = fontColor,
|
||||||
.padding(
|
lineHeight = lineHeight,
|
||||||
top = if (index == 0) 18.dp else 0.dp,
|
|
||||||
start = sidePadding,
|
|
||||||
end = sidePadding,
|
|
||||||
bottom = if (index == state.value.text.lastIndex) 18.dp
|
|
||||||
else paragraphHeight
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
color = fontColor,
|
|
||||||
style = TextStyle(lineBreak = LineBreak.Paragraph),
|
|
||||||
fontFamily = fontFamily.font,
|
|
||||||
fontStyle = fontStyle,
|
fontStyle = fontStyle,
|
||||||
fontSize = mainState.value.fontSize!!.sp,
|
fontSize = mainState.value.fontSize!!.sp,
|
||||||
lineHeight = lineHeight
|
sidePadding = sidePadding,
|
||||||
|
paragraphIndentation = mainState.value.paragraphIndentation!!,
|
||||||
|
onEvent = onEvent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (state.value.text.isNotEmpty()) {
|
if (state.value.text.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
DisableSelection {
|
DisableSelection {
|
||||||
ReaderEndItem(
|
ReaderEndItem(
|
||||||
state = state,
|
state = state,
|
||||||
onEvent = onEvent,
|
onEvent = onEvent,
|
||||||
onLibraryEvent = onLibraryEvent,
|
onLibraryEvent = onLibraryEvent,
|
||||||
onHistoryUpdateEvent = onHistoryEvent,
|
onHistoryUpdateEvent = onHistoryEvent,
|
||||||
listState = listState
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -417,7 +443,6 @@ private fun ReaderScreen(
|
||||||
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
||||||
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
|
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
|
||||||
},
|
},
|
||||||
listState = listState,
|
|
||||||
navigate = {
|
navigate = {
|
||||||
it.navigateBack()
|
it.navigateBack()
|
||||||
}
|
}
|
||||||
|
|
@ -452,7 +477,6 @@ private fun ReaderScreen(
|
||||||
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
||||||
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
|
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
|
||||||
},
|
},
|
||||||
listState = listState,
|
|
||||||
navigate = {
|
navigate = {
|
||||||
it.navigateBack()
|
it.navigateBack()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,10 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
|
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -40,6 +39,11 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reader end item. Displays at the end of the book.
|
* Reader end item. Displays at the end of the book.
|
||||||
|
*
|
||||||
|
* @param state [ReaderState].
|
||||||
|
* @param onEvent [ReaderEvent] callback.
|
||||||
|
* @param onLibraryEvent [LibraryEvent] callback.
|
||||||
|
* @param onHistoryUpdateEvent [HistoryEvent] callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -48,7 +52,6 @@ fun ReaderEndItem(
|
||||||
onEvent: (ReaderEvent) -> Unit,
|
onEvent: (ReaderEvent) -> Unit,
|
||||||
onLibraryEvent: (LibraryEvent) -> Unit,
|
onLibraryEvent: (LibraryEvent) -> Unit,
|
||||||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
||||||
listState: LazyListState,
|
|
||||||
) {
|
) {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val context = LocalContext.current as ComponentActivity
|
val context = LocalContext.current as ComponentActivity
|
||||||
|
|
@ -62,7 +65,7 @@ fun ReaderEndItem(
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = 36.dp,
|
horizontal = 36.dp,
|
||||||
|
|
@ -104,7 +107,6 @@ fun ReaderEndItem(
|
||||||
onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it))
|
onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it))
|
||||||
},
|
},
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
listState = listState,
|
|
||||||
context = context
|
context = context
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -123,7 +125,6 @@ fun ReaderEndItem(
|
||||||
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
||||||
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
||||||
},
|
},
|
||||||
listState = listState,
|
|
||||||
navigate = {
|
navigate = {
|
||||||
it.navigate(Screen.LIBRARY, true)
|
it.navigate(Screen.LIBRARY, true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.SliderDefaults
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
|
@ -33,6 +32,12 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reader bottom bar. Has a slider to change progress.
|
* Reader bottom bar. Has a slider to change progress.
|
||||||
|
*
|
||||||
|
* @param state [ReaderState].
|
||||||
|
* @param onEvent [ReaderEvent] callback.
|
||||||
|
* @param onLibraryUpdateEvent [LibraryEvent] callback.
|
||||||
|
* @param onHistoryUpdateEvent [HistoryEvent] callback.
|
||||||
|
* @param containerColor Container color.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderBottomBar(
|
fun ReaderBottomBar(
|
||||||
|
|
@ -40,7 +45,6 @@ fun ReaderBottomBar(
|
||||||
onEvent: (ReaderEvent) -> Unit,
|
onEvent: (ReaderEvent) -> Unit,
|
||||||
onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
|
onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
|
||||||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
||||||
listState: LazyListState,
|
|
||||||
containerColor: Color
|
containerColor: Color
|
||||||
) {
|
) {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
|
|
@ -79,13 +83,13 @@ fun ReaderBottomBar(
|
||||||
value = state.value.book.progress,
|
value = state.value.book.progress,
|
||||||
enabled = !state.value.lockMenu,
|
enabled = !state.value.lockMenu,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
if (state.value.listState.layoutInfo.totalItemsCount > 0) {
|
||||||
onEvent(ReaderEvent.OnScroll(listState, it))
|
onEvent(ReaderEvent.OnScroll(it))
|
||||||
onEvent(
|
onEvent(
|
||||||
ReaderEvent.OnChangeProgress(
|
ReaderEvent.OnChangeProgress(
|
||||||
progress = it,
|
progress = it,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
firstVisibleItemIndex = listState.firstVisibleItemIndex,
|
firstVisibleItemIndex = state.value.listState.firstVisibleItemIndex,
|
||||||
firstVisibleItemOffset = 0,
|
firstVisibleItemOffset = 0,
|
||||||
refreshList = { book ->
|
refreshList = { book ->
|
||||||
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(book))
|
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(book))
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
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
|
||||||
|
|
@ -39,6 +40,12 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reader top bar. Displays title of the book.
|
* Reader top bar. Displays title of the book.
|
||||||
|
*
|
||||||
|
* @param state [ReaderState].
|
||||||
|
* @param onEvent [ReaderEvent] callback.
|
||||||
|
* @param onLibraryUpdateEvent [LibraryEvent] callback.
|
||||||
|
* @param onHistoryUpdateEvent [HistoryEvent] callback.
|
||||||
|
* @param containerColor Container color.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -47,7 +54,6 @@ fun ReaderTopBar(
|
||||||
onEvent: (ReaderEvent) -> Unit,
|
onEvent: (ReaderEvent) -> Unit,
|
||||||
onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
|
onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
|
||||||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
||||||
listState: LazyListState,
|
|
||||||
containerColor: Color
|
containerColor: Color
|
||||||
) {
|
) {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
|
|
@ -78,7 +84,6 @@ fun ReaderTopBar(
|
||||||
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
|
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
|
||||||
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
||||||
},
|
},
|
||||||
listState = listState,
|
|
||||||
navigate = {
|
navigate = {
|
||||||
it.navigateBack()
|
it.navigateBack()
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +98,7 @@ fun ReaderTopBar(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -109,7 +115,6 @@ fun ReaderTopBar(
|
||||||
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
|
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
|
||||||
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
||||||
},
|
},
|
||||||
listState = listState,
|
|
||||||
navigate = {
|
navigate = {
|
||||||
it.navigateWithoutBackStack(
|
it.navigateWithoutBackStack(
|
||||||
Screen.BOOK_INFO,
|
Screen.BOOK_INFO,
|
||||||
|
|
@ -121,7 +126,7 @@ fun ReaderTopBar(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.basicMarquee(
|
.basicMarquee(
|
||||||
delayMillis = 4000
|
iterations = Int.MAX_VALUE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -135,6 +140,17 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -4,30 +4,22 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.BottomSheetDefaults
|
import androidx.compose.material3.BottomSheetDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
@ -38,6 +30,7 @@ import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.ButtonItem
|
import ua.acclorite.book_story.domain.model.ButtonItem
|
||||||
import ua.acclorite.book_story.domain.util.Constants
|
import ua.acclorite.book_story.domain.util.Constants
|
||||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomBottomSheet
|
||||||
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.screens.reader.data.ReaderEvent
|
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
|
||||||
|
|
@ -48,10 +41,12 @@ import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings bottom sheet. Has General and Colors categories.
|
* Settings bottom sheet. Has General and Colors categories.
|
||||||
|
*
|
||||||
|
* @param mainState [MainState].
|
||||||
|
* @param onEvent [ReaderEvent] callback.
|
||||||
|
* @param onMainEvent [MainEvent] callback.
|
||||||
*/
|
*/
|
||||||
@OptIn(
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
ExperimentalMaterial3Api::class
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ReaderSettingsBottomSheet(
|
fun ReaderSettingsBottomSheet(
|
||||||
mainState: State<MainState>,
|
mainState: State<MainState>,
|
||||||
|
|
@ -62,9 +57,6 @@ fun ReaderSettingsBottomSheet(
|
||||||
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
|
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val navigationBarPadding =
|
|
||||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
|
||||||
|
|
||||||
val fontFamily = remember(mainState.value.fontFamily) {
|
val fontFamily = remember(mainState.value.fontFamily) {
|
||||||
Constants.FONTS.find {
|
Constants.FONTS.find {
|
||||||
it.id == mainState.value.fontFamily
|
it.id == mainState.value.fontFamily
|
||||||
|
|
@ -98,20 +90,16 @@ fun ReaderSettingsBottomSheet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
CustomBottomSheet(
|
||||||
ModalBottomSheet(
|
hasFixedHeight = true,
|
||||||
scrimColor = animatedScrimColor,
|
scrimColor = animatedScrimColor,
|
||||||
dragHandle = {},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(animatedHeight),
|
.fillMaxHeight(animatedHeight),
|
||||||
|
dragHandle = {},
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
|
onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
|
||||||
},
|
}
|
||||||
sheetState = rememberModalBottomSheetState(true),
|
|
||||||
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
|
||||||
) {
|
) {
|
||||||
ReaderSettingsBottomSheetTabRow(
|
ReaderSettingsBottomSheetTabRow(
|
||||||
onEvent = onEvent,
|
onEvent = onEvent,
|
||||||
|
|
@ -298,14 +286,13 @@ fun ReaderSettingsBottomSheet(
|
||||||
item {
|
item {
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier.height(
|
modifier = Modifier.height(
|
||||||
8.dp + navigationBarPadding
|
8.dp + it
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reader start item. Displays at the beginning of the book.
|
* Reader start item. Displays at the beginning of the book.
|
||||||
|
*
|
||||||
|
* @param state [ReaderState].
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -76,8 +78,10 @@ fun ReaderStartItem(state: State<ReaderState>) {
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(150.dp)
|
.height(150.dp)
|
||||||
.width(100.dp)
|
.width(100.dp)
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.background(
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
RoundedCornerShape(10.dp)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (state.value.book.coverImage != null) {
|
if (state.value.book.coverImage != null) {
|
||||||
CustomCoverImage(
|
CustomCoverImage(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
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.derivedStateOf
|
||||||
|
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 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,
|
||||||
|
onEvent: (ReaderEvent) -> Unit
|
||||||
|
) {
|
||||||
|
val text = remember(
|
||||||
|
paragraphIndentation,
|
||||||
|
line.originalLine,
|
||||||
|
line.translatedLine,
|
||||||
|
line.useTranslation,
|
||||||
|
line.translatingTo,
|
||||||
|
line.translatingFrom
|
||||||
|
) {
|
||||||
|
derivedStateOf {
|
||||||
|
if (paragraphIndentation) " "
|
||||||
|
else "" +
|
||||||
|
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 {
|
||||||
|
append(text.value)
|
||||||
|
|
||||||
|
if (showTrailingContent) {
|
||||||
|
append(" ")
|
||||||
|
appendInlineContent("translator")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.then(
|
||||||
|
if (
|
||||||
|
state.value.book.enableTranslator &&
|
||||||
|
state.value.book.doubleClickTranslation
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
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.ExperimentalMaterial3Api
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,29 +1,70 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.reader.data
|
package ua.acclorite.book_story.presentation.screens.reader.data
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
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.UIText
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed class ReaderEvent {
|
sealed class ReaderEvent {
|
||||||
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
|
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
|
||||||
data class OnLoadText(
|
data class OnLoadText(
|
||||||
val listState: LazyListState,
|
|
||||||
val refreshList: (Book) -> Unit,
|
val refreshList: (Book) -> Unit,
|
||||||
val onLoaded: () -> Unit,
|
val onLoaded: () -> Unit,
|
||||||
|
val onError: (UIText) -> Unit,
|
||||||
val onTextIsEmpty: () -> Unit
|
val onTextIsEmpty: () -> Unit
|
||||||
) : ReaderEvent()
|
) : ReaderEvent()
|
||||||
|
|
||||||
data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) :
|
data class OnShowHideMenu(val show: Boolean? = null, 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 navigator: Navigator,
|
val navigator: Navigator,
|
||||||
val listState: LazyListState,
|
|
||||||
val refreshList: (Book) -> Unit,
|
val refreshList: (Book) -> Unit,
|
||||||
val navigate: (Navigator) -> Unit
|
val navigate: (Navigator) -> Unit
|
||||||
) : ReaderEvent()
|
) : ReaderEvent()
|
||||||
|
|
@ -36,18 +77,17 @@ sealed class ReaderEvent {
|
||||||
val refreshList: (Book) -> Unit
|
val refreshList: (Book) -> Unit
|
||||||
) : ReaderEvent()
|
) : ReaderEvent()
|
||||||
|
|
||||||
data class OnScroll(val listState: LazyListState, val progress: Float) : ReaderEvent()
|
data class OnScroll(val progress: Float) : ReaderEvent()
|
||||||
data object OnShowHideSettingsBottomSheet : ReaderEvent()
|
data object OnShowHideSettingsBottomSheet : ReaderEvent()
|
||||||
data class OnScrollToSettingsPage(val page: Int, val pagerState: PagerState) : ReaderEvent()
|
data class OnScrollToSettingsPage(val page: Int, val pagerState: PagerState) : ReaderEvent()
|
||||||
data class OnMoveBookToAlreadyRead(
|
data class OnMoveBookToAlreadyRead(
|
||||||
val context: ComponentActivity,
|
val context: ComponentActivity,
|
||||||
val listState: LazyListState,
|
|
||||||
val onUpdateCategories: (Book) -> Unit,
|
val onUpdateCategories: (Book) -> Unit,
|
||||||
val updatePage: (Int) -> Unit,
|
val updatePage: (Int) -> Unit,
|
||||||
val navigator: Navigator
|
val navigator: Navigator
|
||||||
) : ReaderEvent()
|
) : ReaderEvent()
|
||||||
|
|
||||||
data class OnTranslateText(
|
data class OnOpenTranslator(
|
||||||
val textToTranslate: String,
|
val textToTranslate: String,
|
||||||
val context: ComponentActivity,
|
val context: ComponentActivity,
|
||||||
val noAppsFound: () -> Unit
|
val noAppsFound: () -> Unit
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,35 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.reader.data
|
package ua.acclorite.book_story.presentation.screens.reader.data
|
||||||
|
|
||||||
|
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.StringWithId
|
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||||
|
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.UIText
|
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: List<StringWithId> = emptyList(),
|
val text: Map<ID, LineWithTranslation> = emptyMap(),
|
||||||
val words: Int = 0,
|
val words: Int = 0,
|
||||||
val letters: Int = 0,
|
val letters: Int = 0,
|
||||||
|
val listState: LazyListState = LazyListState(),
|
||||||
|
|
||||||
|
val languagesToTranslate: List<SelectableLanguage> = emptyList(),
|
||||||
|
val showDownloadLanguageDialog: Boolean = false,
|
||||||
|
val isTranslating: Boolean = false,
|
||||||
|
|
||||||
val errorMessage: UIText? = null,
|
val errorMessage: UIText? = null,
|
||||||
|
|
||||||
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,
|
||||||
)
|
)
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,4 +15,9 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
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,6 +11,9 @@ 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
|
||||||
|
|
@ -21,6 +24,9 @@ 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) {
|
||||||
|
|
@ -71,6 +77,17 @@ class SettingsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet -> {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
showReaderTranslatorLanguageBottomSheet = event.show
|
||||||
|
?: !it.showReaderTranslatorLanguageBottomSheet,
|
||||||
|
readerTranslatorLanguageBottomSheetTranslateFrom = event.translateFrom
|
||||||
|
?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,14 @@ import ua.acclorite.book_story.presentation.ui.ThemeContrast
|
||||||
import ua.acclorite.book_story.presentation.ui.animatedColorScheme
|
import ua.acclorite.book_story.presentation.ui.animatedColorScheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme switcher item.
|
* Theme switcher item. Used to change app theme.
|
||||||
|
*
|
||||||
|
* @param theme Target [Theme].
|
||||||
|
* @param darkTheme Whether should be shown in Dark Theme.
|
||||||
|
* @param isPureDark Whether should be shown in Pure Dark.
|
||||||
|
* @param themeContrast [ThemeContrast].
|
||||||
|
* @param selected True if this theme is currently selected.
|
||||||
|
* @param onClick OnClick callback.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppearanceSettingsThemeSwitcherItem(
|
fun AppearanceSettingsThemeSwitcherItem(
|
||||||
|
|
@ -69,7 +76,8 @@ fun AppearanceSettingsThemeSwitcherItem(
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
.background(
|
.background(
|
||||||
colorScheme.surface
|
colorScheme.surface,
|
||||||
|
MaterialTheme.shapes.large
|
||||||
)
|
)
|
||||||
.border(
|
.border(
|
||||||
4.dp,
|
4.dp,
|
||||||
|
|
@ -87,10 +95,9 @@ fun AppearanceSettingsThemeSwitcherItem(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(10.dp))
|
|
||||||
.height(20.dp)
|
.height(20.dp)
|
||||||
.width(80.dp)
|
.width(80.dp)
|
||||||
.background(colorScheme.onSurface)
|
.background(colorScheme.onSurface, RoundedCornerShape(10.dp))
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
|
@ -117,9 +124,9 @@ fun AppearanceSettingsThemeSwitcherItem(
|
||||||
.padding(start = 8.dp)
|
.padding(start = 8.dp)
|
||||||
.height(80.dp)
|
.height(80.dp)
|
||||||
.width(70.dp)
|
.width(70.dp)
|
||||||
.clip(RoundedCornerShape(14.dp))
|
|
||||||
.background(
|
.background(
|
||||||
colorScheme.surfaceContainer
|
colorScheme.surfaceContainer,
|
||||||
|
RoundedCornerShape(14.dp)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
@ -135,16 +142,14 @@ fun AppearanceSettingsThemeSwitcherItem(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.clip(CircleShape)
|
|
||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
.background(colorScheme.primary)
|
.background(colorScheme.primary, CircleShape)
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.clip(RoundedCornerShape(10.dp))
|
|
||||||
.height(20.dp)
|
.height(20.dp)
|
||||||
.width(60.dp)
|
.width(60.dp)
|
||||||
.background(colorScheme.onSurfaceVariant)
|
.background(colorScheme.onSurfaceVariant, RoundedCornerShape(10.dp))
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ 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
|
||||||
|
|
@ -30,26 +31,36 @@ import ua.acclorite.book_story.domain.util.Constants
|
||||||
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.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
|
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,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
onMainEvent = mainViewModel::onEvent
|
onMainEvent = mainViewModel::onEvent,
|
||||||
|
onEvent = settingsViewModel::onEvent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,16 +68,70 @@ fun ReaderSettingsRoot() {
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReaderSettings(
|
private fun ReaderSettings(
|
||||||
state: State<MainState>,
|
state: State<MainState>,
|
||||||
|
settingsState: State<SettingsState>,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
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
|
||||||
|
|
@ -99,6 +164,91 @@ 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),
|
||||||
|
|
@ -108,6 +258,7 @@ private fun ReaderSettings(
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
ChipsWithTitle(
|
ChipsWithTitle(
|
||||||
title = stringResource(id = R.string.font_family_option),
|
title = stringResource(id = R.string.font_family_option),
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,14 @@ import ua.acclorite.book_story.presentation.screens.start.data.StartState
|
||||||
import ua.acclorite.book_story.presentation.ui.Theme
|
import ua.acclorite.book_story.presentation.ui.Theme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings screen.
|
* Start Settings.
|
||||||
|
*
|
||||||
|
* @param state [StartState].
|
||||||
|
* @param mainState [MainState].
|
||||||
|
* @param onEvent [StartEvent] callback.
|
||||||
|
* @param onMainEvent [MainEvent] callback.
|
||||||
|
* @param storagePermissionState Storage [PermissionState].
|
||||||
|
* @param notificationsPermissionState Notifications [PermissionState].
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -123,7 +130,10 @@ fun StartSettings(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.large)
|
.clip(MaterialTheme.shapes.large)
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLow),
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
MaterialTheme.shapes.large
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
StartNavigationScreenItem(
|
StartNavigationScreenItem(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
|
||||||
BIN
app/src/main/res/drawable/empty_languages.png
Normal file
BIN
app/src/main/res/drawable/empty_languages.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -36,6 +36,8 @@
|
||||||
<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">
|
||||||
|
|
@ -78,6 +80,17 @@
|
||||||
Натиснувши на кнопку «Завантажити», ви перейдете в ваш браузер
|
Натиснувши на кнопку «Завантажити», ви перейдете в ваш браузер
|
||||||
та завантажете останнє оновлення автоматично.
|
та завантажете останнє оновлення автоматично.
|
||||||
</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">
|
||||||
|
|
@ -104,6 +117,12 @@
|
||||||
Щось пішло не так.
|
Щось пішло не так.
|
||||||
Спробуйте пізніше.
|
Спробуйте пізніше.
|
||||||
</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>
|
||||||
|
|
||||||
<!-- Is Empty messages -->
|
<!-- Is Empty messages -->
|
||||||
<string name="browse_empty">
|
<string name="browse_empty">
|
||||||
|
|
@ -116,6 +135,9 @@
|
||||||
Нехай ваша остання прочитана книга відкриє перед
|
Нехай ваша остання прочитана книга відкриє перед
|
||||||
вами сторінки нової великої історії вашого читацького шляху
|
вами сторінки нової великої історії вашого читацького шляху
|
||||||
</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>
|
||||||
|
|
@ -150,11 +172,14 @@
|
||||||
<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>
|
||||||
|
|
@ -181,13 +206,14 @@
|
||||||
<string name="general_settings">Головні</string>
|
<string name="general_settings">Головні</string>
|
||||||
<string name="general_settings_desc">Мова застосунка</string>
|
<string name="general_settings_desc">Мова застосунка</string>
|
||||||
<string name="appearance_settings">Зовнішній вигляд</string>
|
<string name="appearance_settings">Зовнішній вигляд</string>
|
||||||
<string name="appearance_settings_desc">Налаштування теми, кольори</string>
|
<string name="appearance_settings_desc">Тема, кольори</string>
|
||||||
<string name="reader_settings">Читач</string>
|
<string name="reader_settings">Читач</string>
|
||||||
<string name="reader_settings_desc">Налаштування шрифта, налаштування текста</string>
|
<string name="reader_settings_desc">Перекладач, шрифт, текст</string>
|
||||||
|
|
||||||
<!-- Settings subcategories -->
|
<!-- Settings subcategories -->
|
||||||
<string name="font_reader_settings">Шрифт</string>
|
<string name="font_reader_settings">Шрифт</string>
|
||||||
<string name="text_reader_settings">Текст</string>
|
<string name="text_reader_settings">Текст</string>
|
||||||
|
<string name="translator_reader_settings">Перекладач</string>
|
||||||
<string name="theme_appearance_settings">Налаштування теми</string>
|
<string name="theme_appearance_settings">Налаштування теми</string>
|
||||||
<string name="colors_appearance_settings">Кольори</string>
|
<string name="colors_appearance_settings">Кольори</string>
|
||||||
|
|
||||||
|
|
@ -208,6 +234,13 @@
|
||||||
<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_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>
|
||||||
|
|
@ -255,7 +288,12 @@
|
||||||
<string name="book_moved">Ця книга була успішно переміщена.</string>
|
<string name="book_moved">Ця книга була успішно переміщена.</string>
|
||||||
<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>
|
||||||
|
|
||||||
<!-- Snackbar messages -->
|
<!-- Snackbar messages -->
|
||||||
<string name="history_element_deleted">Елемент історії був успішно видалений.</string>
|
<string name="history_element_deleted">Елемент історії був успішно видалений.</string>
|
||||||
|
|
@ -282,14 +320,25 @@
|
||||||
Дякуємо за те, що прочитали \"%1$s\" з нами.
|
Дякуємо за те, що прочитали \"%1$s\" з нами.
|
||||||
Ми сподіваємось, що вам сподобалася ця книга!
|
Ми сподіваємось, що вам сподобалася ця книга!
|
||||||
</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">Версія v%1$s</string>
|
<string name="app_version_option_desc_1">Версія v%1$s</string>
|
||||||
<string name="app_version_option_desc_2">Натисніть, щоб перевірити на оновлення</string>
|
<string name="app_version_option_desc_2">Натисніть, щоб перевірити на оновлення</string>
|
||||||
<string name="report_bug_option">Повідомити про помилку</string>
|
<string name="report_bug_option">Повідомити про помилку</string>
|
||||||
<string name="project_page_option">Сторінка проєкту</string>
|
<string name="project_page_option">Сторінка проєкта</string>
|
||||||
<string name="support_option">Підтримати мою роботу</string>
|
<string name="support_option">Підтримати мою роботу</string>
|
||||||
<string name="licenses_option">Ліцензії з відкритим кодом</string>
|
<string name="licenses_option">Ліцензії з відкритим кодом</string>
|
||||||
<string name="credits_option">Заслуги</string>
|
<string name="credits_option">Заслуги</string>
|
||||||
|
|
@ -474,6 +523,8 @@
|
||||||
<string name="reset_start_content_desc">Скинути початковий посібник</string>
|
<string name="reset_start_content_desc">Скинути початковий посібник</string>
|
||||||
<string name="hide_edit_title_content_desc">Вийти з редагування назви</string>
|
<string name="hide_edit_title_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,6 +55,8 @@
|
||||||
<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">
|
||||||
|
|
@ -97,6 +99,17 @@
|
||||||
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">
|
||||||
|
|
@ -123,6 +136,12 @@
|
||||||
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>
|
||||||
|
|
||||||
<!-- Is Empty messages -->
|
<!-- Is Empty messages -->
|
||||||
<string name="browse_empty">
|
<string name="browse_empty">
|
||||||
|
|
@ -134,6 +153,9 @@
|
||||||
<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>
|
||||||
|
|
@ -168,11 +190,14 @@
|
||||||
<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>
|
||||||
|
|
@ -199,13 +224,14 @@
|
||||||
<string name="general_settings">General</string>
|
<string name="general_settings">General</string>
|
||||||
<string name="general_settings_desc">App language</string>
|
<string name="general_settings_desc">App language</string>
|
||||||
<string name="appearance_settings">Appearance</string>
|
<string name="appearance_settings">Appearance</string>
|
||||||
<string name="appearance_settings_desc">Theme preferences, colors</string>
|
<string name="appearance_settings_desc">Theme, colors</string>
|
||||||
<string name="reader_settings">Reader</string>
|
<string name="reader_settings">Reader</string>
|
||||||
<string name="reader_settings_desc">Font settings, text settings</string>
|
<string name="reader_settings_desc">Translator, font, text</string>
|
||||||
|
|
||||||
<!-- Settings subcategories -->
|
<!-- Settings subcategories -->
|
||||||
<string name="font_reader_settings">Font</string>
|
<string name="font_reader_settings">Font</string>
|
||||||
<string name="text_reader_settings">Text</string>
|
<string name="text_reader_settings">Text</string>
|
||||||
|
<string name="translator_reader_settings">Translator</string>
|
||||||
<string name="theme_appearance_settings">Theme preferences</string>
|
<string name="theme_appearance_settings">Theme preferences</string>
|
||||||
<string name="colors_appearance_settings">Colors</string>
|
<string name="colors_appearance_settings">Colors</string>
|
||||||
|
|
||||||
|
|
@ -226,6 +252,13 @@
|
||||||
<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_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>
|
||||||
|
|
@ -274,6 +307,13 @@
|
||||||
<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>
|
||||||
|
|
||||||
<!-- Snackbar messages -->
|
<!-- Snackbar messages -->
|
||||||
<string name="history_element_deleted">History element was successfully deleted.</string>
|
<string name="history_element_deleted">History element was successfully deleted.</string>
|
||||||
|
|
@ -302,6 +342,17 @@
|
||||||
</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">Version v%1$s</string>
|
<string name="app_version_option_desc_1">Version v%1$s</string>
|
||||||
|
|
@ -493,5 +544,8 @@
|
||||||
<string name="reset_start_content_desc">Reset start guide</string>
|
<string name="reset_start_content_desc">Reset start guide</string>
|
||||||
<string name="hide_edit_title_content_desc">Exit title editing</string>
|
<string name="hide_edit_title_content_desc">Exit title editing</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>
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.2.0-rc03" apply false
|
id("com.android.application") version "8.3.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.21" apply false
|
id("org.jetbrains.kotlin.android") version "2.0.0" apply false
|
||||||
id("com.google.devtools.ksp") version "1.9.21-1.0.16" apply false
|
id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
|
||||||
|
id("com.google.devtools.ksp") version "2.0.0-1.0.21" apply false
|
||||||
id("com.google.dagger.hilt.android") version "2.50" apply false
|
id("com.google.dagger.hilt.android") version "2.50" apply false
|
||||||
id("com.mikepenz.aboutlibraries.plugin") version "11.1.3" apply false
|
id("com.mikepenz.aboutlibraries.plugin") version "11.1.3" apply false
|
||||||
|
id("androidx.room") version "2.6.1" apply false
|
||||||
|
kotlin("jvm") version "2.0.0" apply true
|
||||||
|
kotlin("plugin.serialization") version "2.0.0" apply false
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue