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
|
|
@ -1,10 +1,13 @@
|
|||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("kotlin-parcelize")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
id("androidx.room")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
@ -24,6 +27,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
room {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
|
|
@ -76,12 +83,12 @@ aboutLibraries {
|
|||
dependencies {
|
||||
|
||||
// Default
|
||||
implementation("androidx.core:core-ktx:1.13.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1")
|
||||
implementation("androidx.activity:activity-compose:1.9.0")
|
||||
|
||||
// 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.animation:animation")
|
||||
implementation("androidx.compose.animation:animation-android")
|
||||
|
|
@ -90,18 +97,18 @@ dependencies {
|
|||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
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.material:material-icons-extended")
|
||||
implementation("androidx.compose.material:material")
|
||||
|
||||
// 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")
|
||||
|
||||
// Dagger - Hilt
|
||||
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")
|
||||
implementation("com.google.dagger:hilt-compiler:latest.release")
|
||||
ksp("androidx.hilt:hilt-compiler:latest.release")
|
||||
|
|
@ -152,4 +159,11 @@ dependencies {
|
|||
// About open source libraries
|
||||
implementation("com.mikepenz:aboutlibraries-core:latest.release")
|
||||
implementation("com.mikepenz:aboutlibraries-compose-m3:latest.release")
|
||||
|
||||
// Translator
|
||||
implementation("com.google.mlkit:translate:latest.release")
|
||||
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.Screen
|
||||
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.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.data.LicensesViewModel
|
||||
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.data.BrowseViewModel
|
||||
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.data.HistoryViewModel
|
||||
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.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.data.SettingsViewModel
|
||||
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.reader.ReaderSettingsRoot
|
||||
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.Transitions
|
||||
import ua.acclorite.book_story.presentation.ui.isDark
|
||||
|
|
@ -66,14 +58,6 @@ class Activity : AppCompatActivity() {
|
|||
private val libraryViewModel: LibraryViewModel by viewModels()
|
||||
private val historyViewModel: HistoryViewModel 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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -102,8 +86,8 @@ class Activity : AppCompatActivity() {
|
|||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
val isLoaded by mainViewModel.isReady.collectAsState()
|
||||
val state by mainViewModel.state.collectAsState()
|
||||
val isLoaded = mainViewModel.isReady.collectAsState()
|
||||
val state = mainViewModel.state.collectAsState()
|
||||
|
||||
val density = LocalDensity.current
|
||||
val imeInsets = WindowInsets.ime
|
||||
|
|
@ -120,15 +104,15 @@ class Activity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
if (isLoaded) {
|
||||
if (isLoaded.value) {
|
||||
BookStoryTheme(
|
||||
theme = state.theme!!,
|
||||
isDark = state.darkTheme!!.isDark(),
|
||||
isPureDark = state.pureDark!!.isPureDark(this),
|
||||
themeContrast = state.themeContrast!!
|
||||
theme = state.value.theme!!,
|
||||
isDark = state.value.darkTheme!!.isDark(),
|
||||
isPureDark = state.value.pureDark!!.isPureDark(this),
|
||||
themeContrast = state.value.themeContrast!!
|
||||
) {
|
||||
NavigationHost(
|
||||
startScreen = if (state.showStartScreen!!) Screen.START
|
||||
startScreen = if (state.value.showStartScreen!!) Screen.START
|
||||
else Screen.LIBRARY
|
||||
) {
|
||||
navigation(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ package ua.acclorite.book_story.data.di
|
|||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import com.google.mlkit.common.model.RemoteModelManager
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentification
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentificationOptions
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentifier
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dagger.Module
|
||||
|
|
@ -41,4 +45,20 @@ object AppModule {
|
|||
"book_db"
|
||||
).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
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
|
|
@ -16,5 +17,10 @@ data class BookEntity(
|
|||
val scrollOffset: Int,
|
||||
val progress: Float,
|
||||
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
|
||||
|
||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
||||
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||
|
||||
interface UpdatesNotificationService {
|
||||
|
||||
fun postNotification(releaseInfo: ReleaseResponse)
|
||||
fun postNotification(releaseInfo: LatestReleaseInfo)
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "updates_channel"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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
|
||||
|
||||
class UpdatesNotificationServiceImpl @Inject constructor(
|
||||
|
|
@ -18,7 +18,7 @@ class UpdatesNotificationServiceImpl @Inject constructor(
|
|||
private val notificationManager = application.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
as NotificationManager
|
||||
|
||||
override fun postNotification(releaseInfo: ReleaseResponse) {
|
||||
override fun postNotification(releaseInfo: LatestReleaseInfo) {
|
||||
val downloadLatestVersionIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(application.getString(R.string.download_latest_release_page))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package ua.acclorite.book_story.data.local.room
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||
|
|
@ -10,8 +11,11 @@ import ua.acclorite.book_story.data.local.dto.HistoryEntity
|
|||
BookEntity::class,
|
||||
HistoryEntity::class
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
version = 2,
|
||||
autoMigrations = [
|
||||
AutoMigration(1, 2)
|
||||
],
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class BookDatabase : RoomDatabase() {
|
||||
abstract val dao: BookDao
|
||||
|
|
|
|||
|
|
@ -20,7 +20,12 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
|||
textPath = book.textPath,
|
||||
description = book.description,
|
||||
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,
|
||||
lastOpened = null,
|
||||
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
|
||||
|
||||
import ua.acclorite.book_story.domain.model.StringWithId
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import java.io.File
|
||||
|
||||
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.pdf.PdfTextParser
|
||||
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.UIText
|
||||
import java.io.File
|
||||
|
|
@ -15,7 +14,7 @@ class TextParserImpl @Inject constructor(
|
|||
private val pdfTextParser: PdfTextParser,
|
||||
private val epubTextParser: EpubTextParser,
|
||||
) : TextParser {
|
||||
override suspend fun parse(file: File): Resource<List<StringWithId>> {
|
||||
override suspend fun parse(file: File): Resource<List<String>> {
|
||||
val fileFormat = ".${file.name.substringAfterLast(".")}"
|
||||
|
||||
if (fileFormat == ".pdf") {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,12 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
coverImage = null,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
) to coverImage
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import org.jsoup.nodes.Document.OutputSettings
|
|||
import org.jsoup.safety.Safelist
|
||||
import ua.acclorite.book_story.R
|
||||
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.UIText
|
||||
import java.io.BufferedReader
|
||||
|
|
@ -21,7 +20,7 @@ import javax.inject.Inject
|
|||
|
||||
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")) {
|
||||
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())
|
||||
parsedText.outputSettings(OutputSettings().prettyPrint(false))
|
||||
|
|
@ -78,15 +77,15 @@ class EpubTextParser @Inject constructor() : TextParser {
|
|||
.split("\n")
|
||||
.forEach {
|
||||
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.Success(stringWithIds)
|
||||
return Resource.Success(strings)
|
||||
} catch (e: Exception) {
|
||||
return Resource.Error(
|
||||
UIText.StringResource(
|
||||
|
|
|
|||
|
|
@ -43,7 +43,12 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
coverImage = null,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
) to null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tom_roush.pdfbox.pdmodel.PDDocument
|
|||
import com.tom_roush.pdfbox.text.PDFTextStripper
|
||||
import ua.acclorite.book_story.R
|
||||
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.UIText
|
||||
import java.io.File
|
||||
|
|
@ -14,7 +13,7 @@ import javax.inject.Inject
|
|||
|
||||
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")) {
|
||||
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)
|
||||
|
||||
val document = PDDocument.load(file)
|
||||
val stringWithIds = mutableListOf<StringWithId>()
|
||||
val strings = mutableListOf<String>()
|
||||
|
||||
val pdfStripper = PDFTextStripper()
|
||||
pdfStripper.paragraphStart = "</br>"
|
||||
|
|
@ -103,14 +102,14 @@ class PdfTextParser @Inject constructor(private val application: Application) :
|
|||
}
|
||||
|
||||
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.Success(stringWithIds)
|
||||
return Resource.Success(strings)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(
|
||||
|
|
|
|||
|
|
@ -31,7 +31,12 @@ class TxtFileParser @Inject constructor() : FileParser {
|
|||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
coverImage = null,
|
||||
enableTranslator = false,
|
||||
translateFrom = "",
|
||||
translateTo = "",
|
||||
doubleClickTranslation = false,
|
||||
translateWhenOpen = false
|
||||
) to null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import ua.acclorite.book_story.R
|
||||
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.UIText
|
||||
import java.io.BufferedReader
|
||||
|
|
@ -14,19 +13,19 @@ import javax.inject.Inject
|
|||
|
||||
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")) {
|
||||
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||
}
|
||||
|
||||
return try {
|
||||
val formattedLines = mutableListOf<StringWithId>()
|
||||
val formattedLines = mutableListOf<String>()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
BufferedReader(FileReader(file)).forEachLine { line ->
|
||||
if (line.isNotBlank()) {
|
||||
formattedLines.add(
|
||||
StringWithId(line)
|
||||
line.trim()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package ua.acclorite.book_story.data.remote
|
||||
|
||||
import retrofit2.http.GET
|
||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
||||
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||
|
||||
interface GithubAPI {
|
||||
@GET("repos/Acclorite/book-story/releases/latest")
|
||||
suspend fun getLatestRelease(): ReleaseResponse
|
||||
suspend fun getLatestRelease(): LatestReleaseInfo
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://api.github.com/"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.squareup.moshi.JsonClass
|
|||
@Keep
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Immutable
|
||||
data class ReleaseResponse(
|
||||
data class LatestReleaseInfo(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "tag_name") val tagName: String
|
||||
)
|
||||
|
|
@ -6,6 +6,13 @@ import android.net.Uri
|
|||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.google.mlkit.common.model.DownloadConditions
|
||||
import com.google.mlkit.common.model.RemoteModelManager
|
||||
import com.google.mlkit.nl.languageid.LanguageIdentifier
|
||||
import com.google.mlkit.nl.translate.TranslateLanguage
|
||||
import com.google.mlkit.nl.translate.TranslateRemoteModel
|
||||
import com.google.mlkit.nl.translate.Translation
|
||||
import com.google.mlkit.nl.translate.TranslatorOptions
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -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.TextParser
|
||||
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.History
|
||||
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.util.Constants
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import ua.acclorite.book_story.domain.util.DataStoreConstants
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Singleton
|
||||
class BookRepositoryImpl @Inject constructor(
|
||||
private val application: Application,
|
||||
private val database: BookDao,
|
||||
private val dataStore: DataStore,
|
||||
private val modelManager: RemoteModelManager,
|
||||
private val languageIdentifier: LanguageIdentifier,
|
||||
|
||||
private val githubAPI: GithubAPI,
|
||||
private val updatesNotificationService: UpdatesNotificationService,
|
||||
|
|
@ -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)
|
||||
|
||||
if (textPath.isBlank() || !textFile.exists()) {
|
||||
|
|
@ -108,7 +122,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
override suspend fun insertBook(
|
||||
book: Book,
|
||||
coverImage: CoverImage?,
|
||||
text: List<StringWithId>
|
||||
text: List<String>
|
||||
): Boolean {
|
||||
val filesDir = application.filesDir
|
||||
val coversDir = File(filesDir, "covers")
|
||||
|
|
@ -135,7 +149,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
withContext(Dispatchers.IO) {
|
||||
FileOutputStream(textPath).use { stream ->
|
||||
text.forEach { line ->
|
||||
stream.write(line.line.toByteArray())
|
||||
stream.write(line.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(
|
||||
textPath = "$booksDir/$textUri",
|
||||
coverImage = if (coverUri.isNotBlank()) {
|
||||
Uri.fromFile(File("$coversDir/$coverUri"))
|
||||
} else null
|
||||
} else null,
|
||||
enableTranslator = enableTranslator,
|
||||
translateFrom = translateFrom,
|
||||
translateTo = translateTo,
|
||||
doubleClickTranslation = doubleClickTranslation
|
||||
)
|
||||
|
||||
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
|
||||
val filesDir = application.filesDir
|
||||
val booksDir = File(filesDir, "books")
|
||||
|
|
@ -220,7 +256,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
withContext(Dispatchers.IO) {
|
||||
FileOutputStream(textPath).use { stream ->
|
||||
text.forEach { line ->
|
||||
stream.write(line.line.toByteArray())
|
||||
stream.write(line.toByteArray())
|
||||
stream.write(System.lineSeparator().toByteArray())
|
||||
}
|
||||
}
|
||||
|
|
@ -604,7 +640,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
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) {
|
||||
try {
|
||||
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 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 androidx.compose.runtime.Immutable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.UUID
|
||||
|
||||
@Parcelize
|
||||
@Immutable
|
||||
data class StringWithId(
|
||||
val line: String,
|
||||
val id: String = UUID.randomUUID().toString()
|
||||
data class LanguageHistory(
|
||||
val id: Int,
|
||||
val languageCode: String
|
||||
) : 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
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import ua.acclorite.book_story.presentation.data.Screen
|
||||
|
||||
@Immutable
|
||||
data class NavigationItem(
|
||||
val title: String,
|
||||
val selectedIcon: Painter,
|
||||
val unselectedIcon: Painter
|
||||
val screen: Screen,
|
||||
@StringRes val title: Int,
|
||||
@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(
|
||||
val book: Book?,
|
||||
val coverImage: CoverImage? = null,
|
||||
val text: List<StringWithId> = emptyList(),
|
||||
val text: List<String> = emptyList(),
|
||||
val fileName: String?,
|
||||
val message: UIText?
|
||||
) {
|
||||
class NotNull(
|
||||
book: Book,
|
||||
coverImage: CoverImage?,
|
||||
text: List<StringWithId>
|
||||
text: List<String>
|
||||
) : NullableBook(
|
||||
book = book,
|
||||
text = text,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package ua.acclorite.book_story.domain.repository
|
|||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
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.History
|
||||
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.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.Resource
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import java.io.File
|
||||
|
|
@ -25,12 +25,12 @@ interface BookRepository {
|
|||
|
||||
suspend fun getBookText(
|
||||
textPath: String
|
||||
): List<StringWithId>
|
||||
): List<String>
|
||||
|
||||
suspend fun insertBook(
|
||||
book: Book,
|
||||
coverImage: CoverImage?,
|
||||
text: List<StringWithId>
|
||||
text: List<String>
|
||||
): Boolean
|
||||
|
||||
suspend fun updateBooks(
|
||||
|
|
@ -39,7 +39,7 @@ interface BookRepository {
|
|||
|
||||
suspend fun updateBookWithText(
|
||||
book: Book,
|
||||
text: List<StringWithId>
|
||||
text: List<String>
|
||||
): Boolean
|
||||
|
||||
suspend fun updateCoverImageOfBook(
|
||||
|
|
@ -51,6 +51,7 @@ interface BookRepository {
|
|||
books: List<Book>
|
||||
)
|
||||
|
||||
|
||||
suspend fun <T> retrieveDataFromDataStore(
|
||||
key: Preferences.Key<T>,
|
||||
defaultValue: T
|
||||
|
|
@ -61,12 +62,14 @@ interface BookRepository {
|
|||
value: T
|
||||
)
|
||||
|
||||
|
||||
suspend fun getAllSettings(scope: CoroutineScope): MainState
|
||||
|
||||
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
||||
|
||||
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>
|
||||
|
||||
|
||||
suspend fun insertHistory(history: List<History>)
|
||||
|
||||
suspend fun getHistory(): Flow<Resource<List<History>>>
|
||||
|
|
@ -81,5 +84,25 @@ interface BookRepository {
|
|||
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
|
||||
|
||||
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 javax.inject.Inject
|
||||
|
||||
class CheckForUpdates @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(postNotification: Boolean): ReleaseResponse? {
|
||||
suspend fun execute(postNotification: Boolean): LatestReleaseInfo? {
|
||||
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
|
||||
|
||||
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.util.ID
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetText @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(textPath: String): List<StringWithId> {
|
||||
return repository.getBookText(textPath = textPath)
|
||||
suspend fun execute(textPath: String): Map<ID, LineWithTranslation> {
|
||||
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
|
||||
|
||||
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.util.CoverImage
|
||||
import javax.inject.Inject
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
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 javax.inject.Inject
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.Credit
|
||||
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
|
||||
|
||||
object Constants {
|
||||
|
|
@ -25,6 +27,31 @@ object Constants {
|
|||
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
|
||||
val THEMES = listOf(
|
||||
Pair(Theme.DYNAMIC, UIText.StringResource(R.string.dynamic_theme)),
|
||||
|
|
@ -255,6 +282,11 @@ object Constants {
|
|||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
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
|
||||
|
||||
object DataStoreConstants {
|
||||
// App settings
|
||||
val LANGUAGE = stringPreferencesKey("language")
|
||||
val THEME = stringPreferencesKey("theme")
|
||||
val DARK_THEME = stringPreferencesKey("dark_theme")
|
||||
|
|
@ -22,4 +23,9 @@ object DataStoreConstants {
|
|||
val PARAGRAPH_INDENTATION = booleanPreferencesKey("paragraph_indentation")
|
||||
val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
|
||||
val SIDE_PADDING = intPreferencesKey("side_padding")
|
||||
val ENABLE_TRANSLATOR = booleanPreferencesKey("enable_translator")
|
||||
val TRANSLATE_FROM = stringPreferencesKey("translate_from")
|
||||
val TRANSLATE_TO = stringPreferencesKey("translate_to")
|
||||
val DOUBLE_CLICK_TRANSLATION = booleanPreferencesKey("double_click_translation")
|
||||
val TRANSLATOR_LANGUAGE_HISTORY = stringPreferencesKey("translator_languages_history")
|
||||
}
|
||||
|
|
@ -3,4 +3,7 @@ package ua.acclorite.book_story.domain.util
|
|||
import android.graphics.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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
sealed class UIText(val string: String?) {
|
||||
@Parcelize
|
||||
@Immutable
|
||||
sealed class UIText(val string: String?) : Parcelable {
|
||||
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
|
||||
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.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
|
@ -20,14 +19,21 @@ import ua.acclorite.book_story.R
|
|||
|
||||
/**
|
||||
* 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
|
||||
fun CustomCheckbox(selected: Boolean, containerColor: Color, size: Dp = 22.dp) {
|
||||
fun CustomCheckbox(
|
||||
selected: Boolean,
|
||||
containerColor: Color,
|
||||
size: Dp = 22.dp
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
tint = if (selected) containerColor else Color.Transparent,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.border(
|
||||
width = if (!selected) 1.dp else 0.dp,
|
||||
if (!selected) MaterialTheme.colorScheme.outline else Color.Transparent,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package ua.acclorite.book_story.presentation.components
|
||||
|
||||
import android.content.ClipData
|
||||
|
|
@ -7,6 +9,7 @@ import android.view.ActionMode
|
|||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
|
@ -15,6 +18,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalTextToolbar
|
||||
|
|
@ -42,13 +46,13 @@ private class CustomTextActionModeCallback(
|
|||
requireNotNull(menu)
|
||||
requireNotNull(mode)
|
||||
|
||||
onTranslateRequested?.let {
|
||||
menu.add(0, MENU_ITEM_TRANSLATE, 0, context.getString(R.string.translate))
|
||||
onCopyRequested?.let {
|
||||
menu.add(0, MENU_ITEM_COPY, 0, context.getString(R.string.copy))
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
||||
}
|
||||
|
||||
onCopyRequested?.let {
|
||||
menu.add(0, MENU_ITEM_COPY, 1, context.getString(R.string.copy))
|
||||
onTranslateRequested?.let {
|
||||
menu.add(0, MENU_ITEM_TRANSLATE, 1, context.getString(R.string.translate))
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +237,7 @@ fun CustomSelectionContainer(
|
|||
CompositionLocalProvider(
|
||||
LocalTextToolbar provides customSelectionToolbar
|
||||
) {
|
||||
SelectionContainer {
|
||||
SelectionContainer(Modifier.fillMaxSize()) {
|
||||
content(isToolbarHidden.value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.domain.util.Constants
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.Screen
|
||||
|
||||
|
|
@ -31,41 +28,13 @@ fun BottomNavigationBar() {
|
|||
}
|
||||
|
||||
NavigationBar {
|
||||
|
||||
BottomNavigationBarItem(
|
||||
item = NavigationItem(
|
||||
stringResource(id = R.string.library_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)
|
||||
}
|
||||
|
||||
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)
|
||||
Constants.NAVIGATION_ITEMS.forEach {
|
||||
BottomNavigationBarItem(
|
||||
item = it,
|
||||
isSelected = currentScreen == it.screen
|
||||
) {
|
||||
navigator.navigate(it.screen, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.unit.dp
|
||||
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].
|
||||
*
|
||||
* @param modifier Modifier.
|
||||
* @param item [NavigationItem].
|
||||
* @param isSelected Whether this screen is currently selected.
|
||||
* @param onClick OnClick callback.
|
||||
*/
|
||||
@Composable
|
||||
fun RowScope.BottomNavigationBarItem(
|
||||
modifier: Modifier = Modifier,
|
||||
item: NavigationItem,
|
||||
tooltipText: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
|
|
@ -33,11 +39,11 @@ fun RowScope.BottomNavigationBarItem(
|
|||
NavigationBarItem(
|
||||
label = {
|
||||
CustomTooltip(
|
||||
text = tooltipText,
|
||||
text = stringResource(id = item.tooltip),
|
||||
padding = 64.dp
|
||||
) {
|
||||
Text(
|
||||
text = item.title,
|
||||
text = stringResource(id = item.title),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
|
@ -47,11 +53,11 @@ fun RowScope.BottomNavigationBarItem(
|
|||
onClick = { onClick() },
|
||||
icon = {
|
||||
CustomTooltip(
|
||||
text = tooltipText,
|
||||
text = stringResource(id = item.tooltip),
|
||||
padding = 32.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
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.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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 items The items of this dialog, use items(..) { .. } or item { .. }.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomDialogWithLazyColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -22,11 +22,8 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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 ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.model.NavigationItem
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.Screen
|
||||
|
||||
|
|
@ -69,38 +66,13 @@ fun BoxScope.CustomNavigationRail() {
|
|||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CustomNavigationRailItem(
|
||||
item = NavigationItem(
|
||||
stringResource(id = R.string.library_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)
|
||||
}
|
||||
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)
|
||||
Constants.NAVIGATION_ITEMS.forEach {
|
||||
CustomNavigationRailItem(
|
||||
item = it,
|
||||
isSelected = currentScreen == it.screen
|
||||
) {
|
||||
navigator.navigate(it.screen, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.unit.dp
|
||||
import ua.acclorite.book_story.domain.model.NavigationItem
|
||||
|
|
@ -14,12 +16,16 @@ import ua.acclorite.book_story.presentation.components.CustomTooltip
|
|||
|
||||
/**
|
||||
* Custom Navigation Rail item.
|
||||
*
|
||||
* @param modifier Modifier.
|
||||
* @param item [NavigationItem].
|
||||
* @param isSelected Whether this screen is currently selected.
|
||||
* @param onClick OnClick callback.
|
||||
*/
|
||||
@Composable
|
||||
fun CustomNavigationRailItem(
|
||||
modifier: Modifier = Modifier,
|
||||
item: NavigationItem,
|
||||
tooltipText: String,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
|
|
@ -31,11 +37,11 @@ fun CustomNavigationRailItem(
|
|||
NavigationRailItem(
|
||||
label = {
|
||||
CustomTooltip(
|
||||
text = tooltipText,
|
||||
text = stringResource(id = item.tooltip),
|
||||
padding = 48.dp
|
||||
) {
|
||||
Text(
|
||||
text = item.title,
|
||||
text = stringResource(id = item.title),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
|
@ -45,11 +51,11 @@ fun CustomNavigationRailItem(
|
|||
onClick = { onClick() },
|
||||
icon = {
|
||||
CustomTooltip(
|
||||
text = tooltipText,
|
||||
text = stringResource(id = item.tooltip),
|
||||
padding = 16.dp
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
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 <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
|
||||
|
||||
import ua.acclorite.book_story.domain.model.LanguageHistory
|
||||
|
||||
sealed class MainEvent {
|
||||
data class OnChangeLanguage(val lang: 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 OnChangeCheckForUpdates(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeSidePadding(val sidePadding: Int) : MainEvent()
|
||||
data class OnChangeEnableTranslator(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeTranslateFrom(val languageCode: String) : MainEvent()
|
||||
data class OnChangeTranslateTo(val languageCode: String) : MainEvent()
|
||||
data class OnChangeDoubleClickTranslation(val bool: Boolean) : MainEvent()
|
||||
data class OnChangeTranslatorLanguageHistory(val history: List<LanguageHistory>) : MainEvent()
|
||||
}
|
||||
|
|
@ -5,6 +5,12 @@ import android.os.Parcel
|
|||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.DataStoreConstants
|
||||
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 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
|
||||
data class MainState(
|
||||
|
|
@ -38,7 +55,13 @@ data class MainState(
|
|||
val showStartScreen: Boolean? = null,
|
||||
val checkForUpdates: Boolean? = null,
|
||||
val sidePadding: Int? = null,
|
||||
val enableTranslator: Boolean? = null,
|
||||
val translateFrom: String? = null,
|
||||
val translateTo: String? = null,
|
||||
val doubleClickTranslation: Boolean? = null,
|
||||
val translatorLanguageHistory: List<LanguageHistory>? = null,
|
||||
) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
// String
|
||||
language = parcel.readString(),
|
||||
|
|
@ -75,6 +98,19 @@ data class MainState(
|
|||
.toInt() == 1 else null,
|
||||
// Int
|
||||
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) {
|
||||
|
|
@ -117,9 +153,21 @@ data class MainState(
|
|||
// Check For Updates
|
||||
parcel.writeByte((if (checkForUpdates != null) 1 else 0).toByte())
|
||||
checkForUpdates?.let { parcel.writeByte(if (it) 1 else 0) }
|
||||
// Side padding
|
||||
// Side Padding
|
||||
parcel.writeByte((if (sidePadding != null) 1 else 0).toByte())
|
||||
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 {
|
||||
|
|
@ -140,7 +188,6 @@ data class MainState(
|
|||
*/
|
||||
fun initialize(data: Map<String, Any>): MainState {
|
||||
DataStoreConstants.apply {
|
||||
|
||||
val language: String = data[LANGUAGE.name] as? String ?: if (
|
||||
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
|
||||
?: 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(
|
||||
language = language,
|
||||
theme = theme.toTheme(),
|
||||
|
|
@ -211,7 +282,12 @@ data class MainState(
|
|||
paragraphHeight = paragraphHeight,
|
||||
paragraphIndentation = paragraphIndentation,
|
||||
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
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.data.remote.dto.ReleaseResponse
|
||||
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
|
||||
|
||||
@Immutable
|
||||
data class AboutState(
|
||||
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.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
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.presentation.components.GoBackButton
|
||||
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.Navigator
|
||||
import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
|
||||
|
|
@ -78,7 +78,7 @@ private fun CreditsScreen(
|
|||
.padding(top = paddingValues.calculateTopPadding()),
|
||||
state = scrollState.second
|
||||
) {
|
||||
items(Constants.CREDITS, key = { it.name }) {
|
||||
customItems(Constants.CREDITS, key = { it.name }) {
|
||||
CreditItem(credit = it) {
|
||||
onAboutNavigateEvent(
|
||||
AboutEvent.OnNavigateToBrowserPage(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ import ua.acclorite.book_story.domain.model.Credit
|
|||
|
||||
/**
|
||||
* Credit Item.
|
||||
*
|
||||
* @param credit [Credit] item.
|
||||
* @param horizontalPadding Horizontal item padding.
|
||||
* @param verticalPadding Vertical item padding.
|
||||
* @param onClick OnClick callback.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -67,7 +72,7 @@ fun CreditItem(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(100))
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(100))
|
||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,10 @@ private fun LicenseInfoScreen(
|
|||
.clickable(enabled = license.licenseContent?.isNotBlank() == true) {
|
||||
showed.value = !showed.value
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.primary,
|
||||
RoundedCornerShape(100)
|
||||
)
|
||||
.padding(vertical = 6.dp, horizontal = 12.dp)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ import com.mikepenz.aboutlibraries.ui.compose.m3.util.author
|
|||
|
||||
/**
|
||||
* License Item.
|
||||
*
|
||||
* @param modifier Modifier.
|
||||
* @param library [Library] item.
|
||||
* @param horizontalPadding Horizontal item padding.
|
||||
* @param verticalPadding Vertical item padding.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -85,7 +90,7 @@ fun LicenseItem(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(100))
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(100))
|
||||
.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
|
||||
|
||||
/**
|
||||
* Info section.
|
||||
* BookInfo's Info section.
|
||||
*
|
||||
* @param state [BookInfoState] instance.
|
||||
* @param onEvent [BookInfoEvent] callback.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
|
@ -65,7 +68,10 @@ fun BookInfoInfoSection(
|
|||
.height(195.dp)
|
||||
.width(130.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
MaterialTheme.shapes.medium
|
||||
)
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
|
|
|
|||
|
|
@ -5,18 +5,11 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HideImage
|
||||
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.State
|
||||
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.unit.dp
|
||||
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.BookInfoState
|
||||
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.
|
||||
*
|
||||
* @param state [BookInfoState] instance.
|
||||
* @param onEvent [BookInfoEvent] callback.
|
||||
* @param onLibraryUpdateEvent [LibraryEvent] callback.
|
||||
* @param onHistoryUpdateEvent [HistoryEvent] callback.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BookInfoChangeCoverBottomSheet(
|
||||
state: State<BookInfoState>,
|
||||
|
|
@ -41,8 +39,6 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val navigationBarPadding =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
val photoPicker = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
onResult = { uri ->
|
||||
|
|
@ -66,16 +62,12 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
}
|
||||
)
|
||||
|
||||
ModalBottomSheet(
|
||||
CustomBottomSheet(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismissRequest = {
|
||||
onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
|
||||
},
|
||||
sheetState = rememberModalBottomSheetState(true),
|
||||
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
) {
|
||||
|
||||
}
|
||||
) { padding ->
|
||||
BookInfoChangeCoverBottomSheetItem(
|
||||
icon = Icons.Default.ImageSearch,
|
||||
text = stringResource(id = R.string.change_cover),
|
||||
|
|
@ -110,7 +102,7 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
|
||||
Spacer(
|
||||
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 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.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.State
|
||||
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.unit.dp
|
||||
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.BookInfoState
|
||||
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.
|
||||
*
|
||||
* @param state [BookInfoState] instance.
|
||||
* @param onEvent [BookInfoEvent] callback.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BookInfoDetailsBottomSheet(
|
||||
state: State<BookInfoState>,
|
||||
onEvent: (BookInfoEvent) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val navigationBarPadding =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
val pattern = remember {
|
||||
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
|
||||
|
|
@ -66,14 +60,11 @@ fun BookInfoDetailsBottomSheet(
|
|||
else ""
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
CustomBottomSheet(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismissRequest = {
|
||||
onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
|
||||
},
|
||||
sheetState = rememberModalBottomSheetState(true),
|
||||
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
}
|
||||
) {
|
||||
BookInfoDetailsBottomSheetItem(
|
||||
title = stringResource(id = R.string.file_name),
|
||||
|
|
@ -151,7 +142,7 @@ fun BookInfoDetailsBottomSheet(
|
|||
|
||||
Spacer(
|
||||
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 ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.domain.model.StringWithId
|
||||
import ua.acclorite.book_story.presentation.data.Navigator
|
||||
|
||||
@Immutable
|
||||
|
|
@ -58,7 +57,7 @@ sealed class BookInfoEvent {
|
|||
data object OnDismissConfirmUpdateDialog : BookInfoEvent()
|
||||
|
||||
data class OnShowConfirmUpdateDialog(
|
||||
val updatedBook: Pair<Book, List<StringWithId>>,
|
||||
val updatedBook: Pair<Book, List<String>>,
|
||||
val authorUpdated: Boolean,
|
||||
val descriptionUpdated: Boolean,
|
||||
val textUpdated: Boolean,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
|
|||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.Category
|
||||
import ua.acclorite.book_story.domain.model.StringWithId
|
||||
import ua.acclorite.book_story.domain.util.Constants
|
||||
|
||||
@Immutable
|
||||
|
|
@ -13,7 +12,7 @@ data class BookInfoState(
|
|||
val isLoadingUpdate: Boolean = false,
|
||||
val isRefreshing: 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 descriptionChanged: Boolean = false,
|
||||
val textChanged: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ class BookInfoViewModel @Inject constructor(
|
|||
val updatedText = updatedBook.text
|
||||
val text = getText.execute(book.textPath)
|
||||
|
||||
if (updatedText.map { it.line } != text.map { it.line }) {
|
||||
if (updatedText != text.map { it.value.originalLine }) {
|
||||
textUpdated = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -41,6 +42,12 @@ import java.util.Locale
|
|||
|
||||
/**
|
||||
* 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)
|
||||
@Composable
|
||||
|
|
@ -52,12 +59,12 @@ fun BrowseFileItem(
|
|||
onLongClick: () -> Unit
|
||||
) {
|
||||
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
|
||||
val lastModified = remember { pattern.format(Date(file.first.lastModified())) }
|
||||
val sizeBytes = remember { file.first.length() }
|
||||
val lastModified = rememberSaveable { pattern.format(Date(file.first.lastModified())) }
|
||||
val sizeBytes = rememberSaveable { file.first.length() }
|
||||
|
||||
val fileSizeKB = remember { if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 }
|
||||
val fileSizeMB = remember { if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 }
|
||||
val fileSize = remember {
|
||||
val fileSizeKB = rememberSaveable { if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 }
|
||||
val fileSizeMB = rememberSaveable { if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 }
|
||||
val fileSize = rememberSaveable {
|
||||
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
||||
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
||||
else "0 KB"
|
||||
|
|
@ -74,7 +81,7 @@ fun BrowseFileItem(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(backgroundColor)
|
||||
.background(backgroundColor, RoundedCornerShape(10.dp))
|
||||
.combinedClickable(
|
||||
onLongClick = {
|
||||
onLongClick()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,14 @@ import java.util.Locale
|
|||
|
||||
/**
|
||||
* 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
|
||||
fun HistoryItem(
|
||||
|
|
@ -73,8 +81,10 @@ fun HistoryItem(
|
|||
modifier = Modifier
|
||||
.height(90.dp)
|
||||
.width(60.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
RoundedCornerShape(10.dp)
|
||||
)
|
||||
) {
|
||||
if (history.book?.coverImage != null) {
|
||||
CustomCoverImage(
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.layout.onGloballyPositioned
|
||||
|
|
@ -179,8 +178,10 @@ private fun LibraryScreen(
|
|||
Text(
|
||||
text = state.value.books.size.toString(),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainer,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 16.sp
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
|||
|
||||
/**
|
||||
* 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)
|
||||
@Composable
|
||||
|
|
@ -86,7 +92,10 @@ fun LibraryBookItem(
|
|||
.aspectRatio(1f / 1.5f)
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
MaterialTheme.shapes.medium
|
||||
)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onCoverImageClick()
|
||||
|
|
@ -124,7 +133,7 @@ fun LibraryBookItem(
|
|||
modifier = Modifier
|
||||
.padding(6.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.background(MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import androidx.compose.runtime.getValue
|
|||
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.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -113,8 +112,10 @@ fun LibraryTabRow(
|
|||
Text(
|
||||
text = tabItem.second.toString(),
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainer,
|
||||
MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -32,7 +29,6 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.domain.util.Constants
|
||||
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
|
||||
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.translator_language.TranslatorLanguageBottomSheet
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
|
|
@ -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.app_bar.ReaderBottomBar
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.download_language_dialog.ReaderDownloadLanguageDialog
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.start_item.ReaderStartItem
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.text.ReaderTextParagraph
|
||||
import ua.acclorite.book_story.presentation.screens.reader.components.translator_bottom_sheet.ReaderTranslatorBottomSheet
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
||||
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||
|
|
@ -88,12 +84,27 @@ fun ReaderScreenRoot() {
|
|||
val libraryViewModel: LibraryViewModel = hiltViewModel()
|
||||
val historyViewModel: HistoryViewModel = hiltViewModel()
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val loading = remember { mutableStateOf(true) }
|
||||
|
||||
val state = viewModel.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) {
|
||||
viewModel.init(
|
||||
navigator,
|
||||
|
|
@ -102,17 +113,37 @@ fun ReaderScreenRoot() {
|
|||
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
|
||||
historyViewModel.onEvent(HistoryEvent.OnLoadList)
|
||||
},
|
||||
listState = listState,
|
||||
onError = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
it.asString(context),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
},
|
||||
onLoaded = {
|
||||
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(
|
||||
state = state,
|
||||
mainState = mainState,
|
||||
listState = listState,
|
||||
loading = loading,
|
||||
navigator = navigator,
|
||||
onEvent = viewModel::onEvent,
|
||||
|
|
@ -122,13 +153,11 @@ fun ReaderScreenRoot() {
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@Composable
|
||||
private fun ReaderScreen(
|
||||
state: State<ReaderState>,
|
||||
mainState: State<MainState>,
|
||||
listState: LazyListState,
|
||||
loading: State<Boolean>,
|
||||
navigator: Navigator,
|
||||
onEvent: (ReaderEvent) -> Unit,
|
||||
|
|
@ -137,13 +166,9 @@ private fun ReaderScreen(
|
|||
onHistoryEvent: (HistoryEvent) -> Unit,
|
||||
) {
|
||||
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) {
|
||||
object : NestedScrollConnection {
|
||||
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) {
|
||||
Constants.FONTS.find {
|
||||
it.id == mainState.value.fontFamily
|
||||
|
|
@ -190,46 +218,45 @@ private fun ReaderScreen(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(canScroll) {
|
||||
if (!canScroll && !state.value.showMenu && !loading.value) {
|
||||
onEvent(ReaderEvent.OnShowHideMenu(context = context))
|
||||
}
|
||||
if (state.value.showTranslatorBottomSheet) {
|
||||
ReaderTranslatorBottomSheet(
|
||||
state = state,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
if (state.value.showLanguageBottomSheet) {
|
||||
TranslatorLanguageBottomSheet(
|
||||
selectedLanguage = if (state.value.languageBottomSheetTranslateFrom) {
|
||||
state.value.book.translateFrom
|
||||
} else state.value.book.translateTo,
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow {
|
||||
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
|
||||
}.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
|
||||
}
|
||||
unselectedLanguage = if (!state.value.languageBottomSheetTranslateFrom) {
|
||||
state.value.book.translateFrom
|
||||
} else state.value.book.translateTo,
|
||||
|
||||
translateFromSelecting = state.value.languageBottomSheetTranslateFrom,
|
||||
onSelect = { from, to ->
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeProgress(
|
||||
progress = progress,
|
||||
navigator = navigator,
|
||||
firstVisibleItemIndex = items.first,
|
||||
firstVisibleItemOffset = items.second,
|
||||
refreshList = { book ->
|
||||
onLibraryEvent(LibraryEvent.OnUpdateBook(book))
|
||||
onHistoryEvent(HistoryEvent.OnUpdateBook(book))
|
||||
}
|
||||
ReaderEvent.OnChangeTranslatorSettings(
|
||||
translateFrom = from,
|
||||
translateTo = to
|
||||
)
|
||||
)
|
||||
onEvent(
|
||||
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
},
|
||||
onDismiss = {
|
||||
onEvent(
|
||||
ReaderEvent.OnShowHideLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (state.value.showSettingsBottomSheet) {
|
||||
ReaderSettingsBottomSheet(
|
||||
mainState = mainState,
|
||||
|
|
@ -237,6 +264,12 @@ private fun ReaderScreen(
|
|||
onMainEvent = onMainEvent
|
||||
)
|
||||
}
|
||||
if (state.value.showDownloadLanguageDialog) {
|
||||
ReaderDownloadLanguageDialog(
|
||||
state = state,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
@ -254,13 +287,13 @@ private fun ReaderScreen(
|
|||
onEvent = onEvent,
|
||||
onLibraryUpdateEvent = onLibraryEvent,
|
||||
onHistoryUpdateEvent = onHistoryEvent,
|
||||
listState = listState,
|
||||
containerColor = systemBarsColor
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
CustomAnimatedVisibility(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
visible = state.value.showMenu,
|
||||
enter = slideInVertically { it },
|
||||
exit = slideOutVertically { it }
|
||||
|
|
@ -270,7 +303,6 @@ private fun ReaderScreen(
|
|||
onEvent = onEvent,
|
||||
onLibraryUpdateEvent = onLibraryEvent,
|
||||
onHistoryUpdateEvent = onHistoryEvent,
|
||||
listState = listState,
|
||||
containerColor = systemBarsColor
|
||||
)
|
||||
}
|
||||
|
|
@ -288,7 +320,7 @@ private fun ReaderScreen(
|
|||
},
|
||||
onTranslateRequested = { textToTranslate ->
|
||||
onEvent(
|
||||
ReaderEvent.OnTranslateText(
|
||||
ReaderEvent.OnOpenTranslator(
|
||||
textToTranslate,
|
||||
context,
|
||||
noAppsFound = {
|
||||
|
|
@ -318,9 +350,10 @@ private fun ReaderScreen(
|
|||
}
|
||||
) { toolbarShowed ->
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
state = state.value.listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(backgroundColor)
|
||||
.then(
|
||||
if (!loading.value && toolbarShowed) {
|
||||
Modifier
|
||||
|
|
@ -336,56 +369,49 @@ private fun ReaderScreen(
|
|||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(paragraphHeight)
|
||||
) {
|
||||
if (state.value.text.isNotEmpty()) {
|
||||
item {
|
||||
DisableSelection {
|
||||
ReaderStartItem(state = state)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
}
|
||||
}
|
||||
|
||||
customItemsIndexed(
|
||||
state.value.text, key = { key -> key.id }
|
||||
) { index, line ->
|
||||
val text = remember(mainState.value.paragraphIndentation, line) {
|
||||
"${if (mainState.value.paragraphIndentation!!) " " else ""}${line.line}"
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
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,
|
||||
fontSize = mainState.value.fontSize!!.sp,
|
||||
lineHeight = lineHeight
|
||||
)
|
||||
}
|
||||
customItems(
|
||||
text, key = { key -> key.first }
|
||||
) { line ->
|
||||
ReaderTextParagraph(
|
||||
state = state,
|
||||
id = line.first,
|
||||
line = line.second,
|
||||
context = context,
|
||||
density = density,
|
||||
fontFamily = fontFamily,
|
||||
fontColor = fontColor,
|
||||
lineHeight = lineHeight,
|
||||
fontStyle = fontStyle,
|
||||
fontSize = mainState.value.fontSize!!.sp,
|
||||
sidePadding = sidePadding,
|
||||
paragraphIndentation = mainState.value.paragraphIndentation!!,
|
||||
onEvent = onEvent
|
||||
)
|
||||
}
|
||||
|
||||
if (state.value.text.isNotEmpty()) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
DisableSelection {
|
||||
ReaderEndItem(
|
||||
state = state,
|
||||
onEvent = onEvent,
|
||||
onLibraryEvent = onLibraryEvent,
|
||||
onHistoryUpdateEvent = onHistoryEvent,
|
||||
listState = listState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -417,7 +443,6 @@ private fun ReaderScreen(
|
|||
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
||||
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
|
||||
},
|
||||
listState = listState,
|
||||
navigate = {
|
||||
it.navigateBack()
|
||||
}
|
||||
|
|
@ -452,7 +477,6 @@ private fun ReaderScreen(
|
|||
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
||||
onHistoryEvent(HistoryEvent.OnUpdateBook(it))
|
||||
},
|
||||
listState = listState,
|
||||
navigate = {
|
||||
it.navigateBack()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.
|
||||
*
|
||||
* @param state [ReaderState].
|
||||
* @param onEvent [ReaderEvent] callback.
|
||||
* @param onLibraryEvent [LibraryEvent] callback.
|
||||
* @param onHistoryUpdateEvent [HistoryEvent] callback.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -48,7 +52,6 @@ fun ReaderEndItem(
|
|||
onEvent: (ReaderEvent) -> Unit,
|
||||
onLibraryEvent: (LibraryEvent) -> Unit,
|
||||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
||||
listState: LazyListState,
|
||||
) {
|
||||
val navigator = LocalNavigator.current
|
||||
val context = LocalContext.current as ComponentActivity
|
||||
|
|
@ -62,7 +65,7 @@ fun ReaderEndItem(
|
|||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(
|
||||
horizontal = 36.dp,
|
||||
|
|
@ -104,7 +107,6 @@ fun ReaderEndItem(
|
|||
onLibraryEvent(LibraryEvent.OnUpdateCurrentPage(it))
|
||||
},
|
||||
navigator = navigator,
|
||||
listState = listState,
|
||||
context = context
|
||||
)
|
||||
)
|
||||
|
|
@ -123,7 +125,6 @@ fun ReaderEndItem(
|
|||
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
|
||||
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
||||
},
|
||||
listState = listState,
|
||||
navigate = {
|
||||
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.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
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.
|
||||
*
|
||||
* @param state [ReaderState].
|
||||
* @param onEvent [ReaderEvent] callback.
|
||||
* @param onLibraryUpdateEvent [LibraryEvent] callback.
|
||||
* @param onHistoryUpdateEvent [HistoryEvent] callback.
|
||||
* @param containerColor Container color.
|
||||
*/
|
||||
@Composable
|
||||
fun ReaderBottomBar(
|
||||
|
|
@ -40,7 +45,6 @@ fun ReaderBottomBar(
|
|||
onEvent: (ReaderEvent) -> Unit,
|
||||
onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
|
||||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
||||
listState: LazyListState,
|
||||
containerColor: Color
|
||||
) {
|
||||
val navigator = LocalNavigator.current
|
||||
|
|
@ -79,13 +83,13 @@ fun ReaderBottomBar(
|
|||
value = state.value.book.progress,
|
||||
enabled = !state.value.lockMenu,
|
||||
onValueChange = {
|
||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
onEvent(ReaderEvent.OnScroll(listState, it))
|
||||
if (state.value.listState.layoutInfo.totalItemsCount > 0) {
|
||||
onEvent(ReaderEvent.OnScroll(it))
|
||||
onEvent(
|
||||
ReaderEvent.OnChangeProgress(
|
||||
progress = it,
|
||||
navigator = navigator,
|
||||
firstVisibleItemIndex = listState.firstVisibleItemIndex,
|
||||
firstVisibleItemIndex = state.value.listState.firstVisibleItemIndex,
|
||||
firstVisibleItemOffset = 0,
|
||||
refreshList = { book ->
|
||||
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(book))
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ import androidx.compose.foundation.basicMarquee
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
|
|
@ -39,6 +40,12 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
|
|||
|
||||
/**
|
||||
* 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)
|
||||
@Composable
|
||||
|
|
@ -47,7 +54,6 @@ fun ReaderTopBar(
|
|||
onEvent: (ReaderEvent) -> Unit,
|
||||
onLibraryUpdateEvent: (LibraryEvent.OnUpdateBook) -> Unit,
|
||||
onHistoryUpdateEvent: (HistoryEvent.OnUpdateBook) -> Unit,
|
||||
listState: LazyListState,
|
||||
containerColor: Color
|
||||
) {
|
||||
val navigator = LocalNavigator.current
|
||||
|
|
@ -78,7 +84,6 @@ fun ReaderTopBar(
|
|||
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
|
||||
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
||||
},
|
||||
listState = listState,
|
||||
navigate = {
|
||||
it.navigateBack()
|
||||
}
|
||||
|
|
@ -93,6 +98,7 @@ fun ReaderTopBar(
|
|||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 20.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
|
|
@ -109,7 +115,6 @@ fun ReaderTopBar(
|
|||
onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it))
|
||||
onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it))
|
||||
},
|
||||
listState = listState,
|
||||
navigate = {
|
||||
it.navigateWithoutBackStack(
|
||||
Screen.BOOK_INFO,
|
||||
|
|
@ -121,7 +126,7 @@ fun ReaderTopBar(
|
|||
}
|
||||
)
|
||||
.basicMarquee(
|
||||
delayMillis = 4000
|
||||
iterations = Int.MAX_VALUE
|
||||
)
|
||||
)
|
||||
Text(
|
||||
|
|
@ -135,6 +140,17 @@ fun ReaderTopBar(
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Translate,
|
||||
contentDescription = R.string.translator_content_desc,
|
||||
disableOnClick = false,
|
||||
enabled = !state.value.lockMenu,
|
||||
color = if (state.value.book.enableTranslator) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else LocalContentColor.current
|
||||
) {
|
||||
onEvent(ReaderEvent.OnShowHideTranslatorBottomSheet())
|
||||
}
|
||||
CustomIconButton(
|
||||
icon = Icons.Default.Settings,
|
||||
contentDescription = R.string.open_reader_settings_content_desc,
|
||||
|
|
|
|||
|
|
@ -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.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
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.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
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.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.util.Constants
|
||||
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.MainState
|
||||
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.
|
||||
*
|
||||
* @param mainState [MainState].
|
||||
* @param onEvent [ReaderEvent] callback.
|
||||
* @param onMainEvent [MainEvent] callback.
|
||||
*/
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class
|
||||
)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReaderSettingsBottomSheet(
|
||||
mainState: State<MainState>,
|
||||
|
|
@ -62,9 +57,6 @@ fun ReaderSettingsBottomSheet(
|
|||
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
|
||||
val context = LocalContext.current
|
||||
|
||||
val navigationBarPadding =
|
||||
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
val fontFamily = remember(mainState.value.fontFamily) {
|
||||
Constants.FONTS.find {
|
||||
it.id == mainState.value.fontFamily
|
||||
|
|
@ -98,210 +90,205 @@ fun ReaderSettingsBottomSheet(
|
|||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ModalBottomSheet(
|
||||
scrimColor = animatedScrimColor,
|
||||
dragHandle = {},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(animatedHeight),
|
||||
onDismissRequest = {
|
||||
onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
|
||||
},
|
||||
sheetState = rememberModalBottomSheetState(true),
|
||||
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
|
||||
) {
|
||||
ReaderSettingsBottomSheetTabRow(
|
||||
onEvent = onEvent,
|
||||
pagerState = pagerState
|
||||
)
|
||||
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
LazyColumn {
|
||||
if (page == 0) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.font_reader_settings),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
item {
|
||||
ChipsWithTitle(
|
||||
title = stringResource(id = R.string.font_family_option),
|
||||
chips = Constants.FONTS
|
||||
.map {
|
||||
ButtonItem(
|
||||
it.id,
|
||||
it.fontName.asString(),
|
||||
MaterialTheme.typography.labelLarge.copy(
|
||||
fontFamily = it.font
|
||||
),
|
||||
it.id == fontFamily.id
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onMainEvent(MainEvent.OnChangeFontFamily(it.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
ChipsWithTitle(
|
||||
title = stringResource(id = R.string.font_style_option),
|
||||
chips = listOf(
|
||||
ButtonItem(
|
||||
"normal",
|
||||
stringResource(id = R.string.font_style_normal),
|
||||
MaterialTheme.typography.labelLarge.copy(
|
||||
fontFamily = fontFamily.font,
|
||||
fontStyle = FontStyle.Normal
|
||||
),
|
||||
!mainState.value.isItalic!!
|
||||
),
|
||||
ButtonItem(
|
||||
"italic",
|
||||
stringResource(id = R.string.font_style_italic),
|
||||
MaterialTheme.typography.labelLarge.copy(
|
||||
fontFamily = fontFamily.font,
|
||||
fontStyle = FontStyle.Italic
|
||||
),
|
||||
mainState.value.isItalic!!
|
||||
),
|
||||
),
|
||||
onClick = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontStyle(
|
||||
when (it.id) {
|
||||
"italic" -> true
|
||||
else -> false
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.fontSize!! to "pt",
|
||||
fromValue = 10,
|
||||
toValue = 35,
|
||||
title = stringResource(id = R.string.font_size_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontSize(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.text_reader_settings),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.sidePadding!! to "pt",
|
||||
fromValue = 1,
|
||||
toValue = 20,
|
||||
title = stringResource(id = R.string.side_padding_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeSidePadding(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.lineHeight!! to "pt",
|
||||
fromValue = 1,
|
||||
toValue = 16,
|
||||
title = stringResource(id = R.string.line_height_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeLineHeight(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.paragraphHeight!! to "pt",
|
||||
fromValue = 0,
|
||||
toValue = 24,
|
||||
title = stringResource(id = R.string.paragraph_height_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeParagraphHeight(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = mainState.value.paragraphIndentation!!,
|
||||
title = stringResource(id = R.string.paragraph_indentation_option)
|
||||
) {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeParagraphIndentation(!mainState.value.paragraphIndentation!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (page == 1) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
}
|
||||
item {
|
||||
ColorPickerWithTitle(
|
||||
value = Color(mainState.value.backgroundColor!!.toULong()),
|
||||
title = stringResource(id = R.string.background_color_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBackgroundColor(
|
||||
it.value.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
ColorPickerWithTitle(
|
||||
value = Color(mainState.value.fontColor!!.toULong()),
|
||||
title = stringResource(id = R.string.font_color_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontColor(
|
||||
it.value.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
CustomBottomSheet(
|
||||
hasFixedHeight = true,
|
||||
scrimColor = animatedScrimColor,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(animatedHeight),
|
||||
dragHandle = {},
|
||||
onDismissRequest = {
|
||||
onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
|
||||
}
|
||||
) {
|
||||
ReaderSettingsBottomSheetTabRow(
|
||||
onEvent = onEvent,
|
||||
pagerState = pagerState
|
||||
)
|
||||
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
LazyColumn {
|
||||
if (page == 0) {
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier.height(
|
||||
8.dp + navigationBarPadding
|
||||
)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.font_reader_settings),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
item {
|
||||
ChipsWithTitle(
|
||||
title = stringResource(id = R.string.font_family_option),
|
||||
chips = Constants.FONTS
|
||||
.map {
|
||||
ButtonItem(
|
||||
it.id,
|
||||
it.fontName.asString(),
|
||||
MaterialTheme.typography.labelLarge.copy(
|
||||
fontFamily = it.font
|
||||
),
|
||||
it.id == fontFamily.id
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onMainEvent(MainEvent.OnChangeFontFamily(it.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
ChipsWithTitle(
|
||||
title = stringResource(id = R.string.font_style_option),
|
||||
chips = listOf(
|
||||
ButtonItem(
|
||||
"normal",
|
||||
stringResource(id = R.string.font_style_normal),
|
||||
MaterialTheme.typography.labelLarge.copy(
|
||||
fontFamily = fontFamily.font,
|
||||
fontStyle = FontStyle.Normal
|
||||
),
|
||||
!mainState.value.isItalic!!
|
||||
),
|
||||
ButtonItem(
|
||||
"italic",
|
||||
stringResource(id = R.string.font_style_italic),
|
||||
MaterialTheme.typography.labelLarge.copy(
|
||||
fontFamily = fontFamily.font,
|
||||
fontStyle = FontStyle.Italic
|
||||
),
|
||||
mainState.value.isItalic!!
|
||||
),
|
||||
),
|
||||
onClick = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontStyle(
|
||||
when (it.id) {
|
||||
"italic" -> true
|
||||
else -> false
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.fontSize!! to "pt",
|
||||
fromValue = 10,
|
||||
toValue = 35,
|
||||
title = stringResource(id = R.string.font_size_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontSize(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.text_reader_settings),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.sidePadding!! to "pt",
|
||||
fromValue = 1,
|
||||
toValue = 20,
|
||||
title = stringResource(id = R.string.side_padding_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeSidePadding(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.lineHeight!! to "pt",
|
||||
fromValue = 1,
|
||||
toValue = 16,
|
||||
title = stringResource(id = R.string.line_height_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeLineHeight(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SliderWithTitle(
|
||||
value = mainState.value.paragraphHeight!! to "pt",
|
||||
fromValue = 0,
|
||||
toValue = 24,
|
||||
title = stringResource(id = R.string.paragraph_height_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeParagraphHeight(it)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = mainState.value.paragraphIndentation!!,
|
||||
title = stringResource(id = R.string.paragraph_indentation_option)
|
||||
) {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeParagraphIndentation(!mainState.value.paragraphIndentation!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (page == 1) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
}
|
||||
item {
|
||||
ColorPickerWithTitle(
|
||||
value = Color(mainState.value.backgroundColor!!.toULong()),
|
||||
title = stringResource(id = R.string.background_color_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeBackgroundColor(
|
||||
it.value.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
ColorPickerWithTitle(
|
||||
value = Color(mainState.value.fontColor!!.toULong()),
|
||||
title = stringResource(id = R.string.font_color_option),
|
||||
onValueChange = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeFontColor(
|
||||
it.value.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier.height(
|
||||
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.
|
||||
*
|
||||
* @param state [ReaderState].
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
|
@ -76,8 +78,10 @@ fun ReaderStartItem(state: State<ReaderState>) {
|
|||
modifier = Modifier
|
||||
.height(150.dp)
|
||||
.width(100.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
RoundedCornerShape(10.dp)
|
||||
)
|
||||
) {
|
||||
if (state.value.book.coverImage != null) {
|
||||
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
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.LineWithTranslation
|
||||
import ua.acclorite.book_story.domain.model.SelectableLanguage
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
import ua.acclorite.book_story.domain.util.LanguageCode
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
import ua.acclorite.book_story.presentation.data.Navigator
|
||||
|
||||
@Immutable
|
||||
sealed class ReaderEvent {
|
||||
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
|
||||
data class OnLoadText(
|
||||
val listState: LazyListState,
|
||||
val refreshList: (Book) -> Unit,
|
||||
val onLoaded: () -> Unit,
|
||||
val onError: (UIText) -> Unit,
|
||||
val onTextIsEmpty: () -> Unit
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) :
|
||||
ReaderEvent()
|
||||
|
||||
data class OnShowHideTranslatorBottomSheet(val show: Boolean? = null) : ReaderEvent()
|
||||
|
||||
data class OnChangeTranslatorSettings(
|
||||
val enableTranslator: Boolean? = null,
|
||||
val translateFrom: String? = null,
|
||||
val translateTo: String? = null,
|
||||
val doubleClickTranslation: Boolean? = null,
|
||||
val translateWhenOpen: Boolean? = null
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnShowHideLanguageBottomSheet(
|
||||
val show: Boolean? = null,
|
||||
val translateFrom: Boolean? = null
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnTranslateText(
|
||||
val keys: Set<ID>?,
|
||||
val error: suspend (UIText) -> Unit
|
||||
) : ReaderEvent()
|
||||
|
||||
data object OnDismissDownloadLanguageDialog : ReaderEvent()
|
||||
|
||||
data object OnCancelTranslation : ReaderEvent()
|
||||
|
||||
data class OnUndoTranslation(val id: ID?) : ReaderEvent()
|
||||
|
||||
data class OnDownloadLanguages(
|
||||
val onSuccess: suspend (Map<ID, LineWithTranslation>) -> Unit,
|
||||
val error: suspend (UIText) -> Unit
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnSelectLanguage(val language: SelectableLanguage) : ReaderEvent()
|
||||
|
||||
data class OnUpdateLanguage(
|
||||
val languageCode: LanguageCode,
|
||||
val calculation: (SelectableLanguage) -> SelectableLanguage
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnGoBack(
|
||||
val context: ComponentActivity,
|
||||
val navigator: Navigator,
|
||||
val listState: LazyListState,
|
||||
val refreshList: (Book) -> Unit,
|
||||
val navigate: (Navigator) -> Unit
|
||||
) : ReaderEvent()
|
||||
|
|
@ -36,18 +77,17 @@ sealed class ReaderEvent {
|
|||
val refreshList: (Book) -> Unit
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnScroll(val listState: LazyListState, val progress: Float) : ReaderEvent()
|
||||
data class OnScroll(val progress: Float) : ReaderEvent()
|
||||
data object OnShowHideSettingsBottomSheet : ReaderEvent()
|
||||
data class OnScrollToSettingsPage(val page: Int, val pagerState: PagerState) : ReaderEvent()
|
||||
data class OnMoveBookToAlreadyRead(
|
||||
val context: ComponentActivity,
|
||||
val listState: LazyListState,
|
||||
val onUpdateCategories: (Book) -> Unit,
|
||||
val updatePage: (Int) -> Unit,
|
||||
val navigator: Navigator
|
||||
) : ReaderEvent()
|
||||
|
||||
data class OnTranslateText(
|
||||
data class OnOpenTranslator(
|
||||
val textToTranslate: String,
|
||||
val context: ComponentActivity,
|
||||
val noAppsFound: () -> Unit
|
||||
|
|
|
|||
|
|
@ -1,23 +1,35 @@
|
|||
package ua.acclorite.book_story.presentation.screens.reader.data
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.model.Book
|
||||
import ua.acclorite.book_story.domain.model.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.ID
|
||||
import ua.acclorite.book_story.domain.util.UIText
|
||||
|
||||
@Immutable
|
||||
data class ReaderState(
|
||||
val book: Book = Constants.EMPTY_BOOK,
|
||||
val text: List<StringWithId> = emptyList(),
|
||||
val text: Map<ID, LineWithTranslation> = emptyMap(),
|
||||
val words: Int = 0,
|
||||
val letters: Int = 0,
|
||||
val listState: LazyListState = LazyListState(),
|
||||
|
||||
val languagesToTranslate: List<SelectableLanguage> = emptyList(),
|
||||
val showDownloadLanguageDialog: Boolean = false,
|
||||
val isTranslating: Boolean = false,
|
||||
|
||||
val errorMessage: UIText? = null,
|
||||
|
||||
val showMenu: Boolean = false,
|
||||
val lockMenu: Boolean = false,
|
||||
|
||||
val showTranslatorBottomSheet: Boolean = false,
|
||||
val showLanguageBottomSheet: Boolean = false,
|
||||
val languageBottomSheetTranslateFrom: Boolean = false,
|
||||
|
||||
val showSettingsBottomSheet: Boolean = false,
|
||||
val currentPage: Int = 0,
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,4 +15,9 @@ sealed class SettingsEvent {
|
|||
val notificationsPermissionState: PermissionState,
|
||||
val onChangeCheckForUpdates: (Boolean) -> Unit
|
||||
) : 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 kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import javax.inject.Inject
|
||||
|
|
@ -21,6 +24,9 @@ class SettingsViewModel @Inject constructor(
|
|||
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(SettingsState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var notificationsPermissionJob: Job? = null
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* 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
|
||||
fun AppearanceSettingsThemeSwitcherItem(
|
||||
|
|
@ -69,7 +76,8 @@ fun AppearanceSettingsThemeSwitcherItem(
|
|||
onClick()
|
||||
}
|
||||
.background(
|
||||
colorScheme.surface
|
||||
colorScheme.surface,
|
||||
MaterialTheme.shapes.large
|
||||
)
|
||||
.border(
|
||||
4.dp,
|
||||
|
|
@ -87,10 +95,9 @@ fun AppearanceSettingsThemeSwitcherItem(
|
|||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.height(20.dp)
|
||||
.width(80.dp)
|
||||
.background(colorScheme.onSurface)
|
||||
.background(colorScheme.onSurface, RoundedCornerShape(10.dp))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
|
|
@ -117,9 +124,9 @@ fun AppearanceSettingsThemeSwitcherItem(
|
|||
.padding(start = 8.dp)
|
||||
.height(80.dp)
|
||||
.width(70.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(
|
||||
colorScheme.surfaceContainer
|
||||
colorScheme.surfaceContainer,
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
|
@ -135,16 +142,14 @@ fun AppearanceSettingsThemeSwitcherItem(
|
|||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.size(20.dp)
|
||||
.background(colorScheme.primary)
|
||||
.background(colorScheme.primary, CircleShape)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.height(20.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.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -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.GoBackButton
|
||||
import ua.acclorite.book_story.presentation.components.collapsibleUntilExitScrollBehaviorWithLazyListState
|
||||
import ua.acclorite.book_story.presentation.components.translator_language.TranslatorLanguageBottomSheet
|
||||
import ua.acclorite.book_story.presentation.data.LocalNavigator
|
||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
||||
import ua.acclorite.book_story.presentation.data.MainState
|
||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||
import ua.acclorite.book_story.presentation.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.SliderWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWithTitle
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
|
||||
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun ReaderSettingsRoot() {
|
||||
val navigator = LocalNavigator.current
|
||||
val mainViewModel: MainViewModel = hiltViewModel()
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
|
||||
val state = mainViewModel.state.collectAsState()
|
||||
val settingsState = settingsViewModel.state.collectAsState()
|
||||
|
||||
ReaderSettings(
|
||||
state = state,
|
||||
settingsState = settingsState,
|
||||
navigator = navigator,
|
||||
onMainEvent = mainViewModel::onEvent
|
||||
onMainEvent = mainViewModel::onEvent,
|
||||
onEvent = settingsViewModel::onEvent
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -57,16 +68,70 @@ fun ReaderSettingsRoot() {
|
|||
@Composable
|
||||
private fun ReaderSettings(
|
||||
state: State<MainState>,
|
||||
settingsState: State<SettingsState>,
|
||||
navigator: Navigator,
|
||||
onMainEvent: (MainEvent) -> Unit
|
||||
onMainEvent: (MainEvent) -> Unit,
|
||||
onEvent: (SettingsEvent) -> Unit
|
||||
) {
|
||||
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
|
||||
val context = LocalContext.current
|
||||
|
||||
val fontFamily = remember(state.value.fontFamily) {
|
||||
Constants.FONTS.find {
|
||||
it.id == state.value.fontFamily
|
||||
} ?: Constants.FONTS[0]
|
||||
}
|
||||
val translateFrom = remember(state.value.translateFrom) {
|
||||
if (state.value.translateFrom!! == "auto") {
|
||||
return@remember context.getString(R.string.translator_auto_short)
|
||||
}
|
||||
|
||||
val locale = Locale(state.value.translateFrom!!)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
val translateTo = remember(state.value.translateTo) {
|
||||
val locale = Locale(state.value.translateTo!!)
|
||||
locale.getDisplayName(locale).replaceFirstChar { char ->
|
||||
char.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsState.value.showReaderTranslatorLanguageBottomSheet) {
|
||||
TranslatorLanguageBottomSheet(
|
||||
selectedLanguage = if (
|
||||
settingsState.value.readerTranslatorLanguageBottomSheetTranslateFrom
|
||||
) {
|
||||
state.value.translateFrom!!
|
||||
} else state.value.translateTo!!,
|
||||
|
||||
unselectedLanguage = if (
|
||||
!settingsState.value.readerTranslatorLanguageBottomSheetTranslateFrom
|
||||
) {
|
||||
state.value.translateFrom!!
|
||||
} else state.value.translateTo!!,
|
||||
|
||||
translateFromSelecting = settingsState.value.readerTranslatorLanguageBottomSheetTranslateFrom,
|
||||
onSelect = { from, to ->
|
||||
onMainEvent(MainEvent.OnChangeTranslateFrom(from))
|
||||
onMainEvent(MainEvent.OnChangeTranslateTo(to))
|
||||
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
},
|
||||
onDismiss = {
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
@ -99,6 +164,91 @@ private fun ReaderSettings(
|
|||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.translator_reader_settings),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = state.value.enableTranslator!!,
|
||||
title = stringResource(id = R.string.translator_enable_default_option),
|
||||
description = stringResource(id = R.string.translator_enable_default_option_desc),
|
||||
onClick = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeEnableTranslator(
|
||||
!state.value.enableTranslator!!
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.translator_language_option)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
ReaderTranslatorBottomSheetLanguageChooser(
|
||||
fromLanguage = translateFrom,
|
||||
toLanguage = translateTo,
|
||||
onFromClick = {
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = true,
|
||||
translateFrom = true
|
||||
)
|
||||
)
|
||||
},
|
||||
onToClick = {
|
||||
onEvent(
|
||||
SettingsEvent.OnReaderShowHideTranslatorLanguageBottomSheet(
|
||||
show = true,
|
||||
translateFrom = false
|
||||
)
|
||||
)
|
||||
},
|
||||
switchEnabled = state.value.translateFrom != "auto",
|
||||
onSwitchClick = {
|
||||
val from = state.value.translateFrom!!
|
||||
val to = state.value.translateTo!!
|
||||
|
||||
onMainEvent(MainEvent.OnChangeTranslateFrom(to))
|
||||
onMainEvent(MainEvent.OnChangeTranslateTo(from))
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
|
||||
item {
|
||||
SwitchWithTitle(
|
||||
selected = state.value.doubleClickTranslation!!,
|
||||
title = stringResource(id = R.string.translator_double_click_to_translate_option),
|
||||
description = stringResource(id = R.string.translator_double_click_to_translate_option_desc),
|
||||
onClick = {
|
||||
onMainEvent(
|
||||
MainEvent.OnChangeDoubleClickTranslation(
|
||||
!state.value.doubleClickTranslation!!
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
item {
|
||||
CategoryTitle(
|
||||
title = stringResource(id = R.string.font_reader_settings),
|
||||
|
|
@ -108,6 +258,7 @@ private fun ReaderSettings(
|
|||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
ChipsWithTitle(
|
||||
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
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@Composable
|
||||
|
|
@ -123,7 +130,10 @@ fun StartSettings(
|
|||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
MaterialTheme.shapes.large
|
||||
),
|
||||
) {
|
||||
StartNavigationScreenItem(
|
||||
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="delete_history">Видалити історію читання?</string>
|
||||
<string name="confirm_update">Підтвердити оновлення?</string>
|
||||
<string name="download_language">Завантажити %1$s?</string>
|
||||
<string name="download_languages">Завантажити мови?</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
|
|
@ -78,6 +80,17 @@
|
|||
Натиснувши на кнопку «Завантажити», ви перейдете в ваш браузер
|
||||
та завантажете останнє оновлення автоматично.
|
||||
</string>
|
||||
<string name="download_language_description">
|
||||
Вам потрібно завантажити цю мову перед використанням.
|
||||
Після завантаження, вона буде доступна офлайн та не потребуватиме інтернету.
|
||||
Кожна мова має розмір близько ~30мб, для завантаження рекомендується використовувати Wi-Fi.
|
||||
</string>
|
||||
<string name="download_languages_description">
|
||||
Вам потрібно завантажити ці мови перед використанням.
|
||||
Кожна мова має розмір близько ~30мб, для завантаження рекомендується використовувати Wi-Fi.
|
||||
Ви можете вибрати мови, які ви хочете завантажити,
|
||||
всі не вибрані мови не будуть перекладені.
|
||||
</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
|
|
@ -104,6 +117,12 @@
|
|||
Щось пішло не так.
|
||||
Спробуйте пізніше.
|
||||
</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 -->
|
||||
<string name="browse_empty">
|
||||
|
|
@ -116,6 +135,9 @@
|
|||
Нехай ваша остання прочитана книга відкриє перед
|
||||
вами сторінки нової великої історії вашого читацького шляху
|
||||
</string>
|
||||
<string name="translator_languages_empty">
|
||||
Не існує мови з такою назвою, або вона не підтримується
|
||||
</string>
|
||||
|
||||
<!-- Query strings -->
|
||||
<string name="selected_items_count_query">%1$s обрано</string>
|
||||
|
|
@ -150,11 +172,14 @@
|
|||
<string name="no">Ні</string>
|
||||
<string name="download">Завантажити</string>
|
||||
<string name="done">Готово</string>
|
||||
<string name="undo_translation">Відмінити</string>
|
||||
<string name="continue_download">Далі</string>
|
||||
|
||||
<!-- Items -->
|
||||
<string name="files">файли</string>
|
||||
<string name="books">книги</string>
|
||||
<string name="history">історію</string>
|
||||
<string name="languages">мови</string>
|
||||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Невідомий</string>
|
||||
|
|
@ -181,13 +206,14 @@
|
|||
<string name="general_settings">Головні</string>
|
||||
<string name="general_settings_desc">Мова застосунка</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_desc">Налаштування шрифта, налаштування текста</string>
|
||||
<string name="reader_settings_desc">Перекладач, шрифт, текст</string>
|
||||
|
||||
<!-- Settings subcategories -->
|
||||
<string name="font_reader_settings">Шрифт</string>
|
||||
<string name="text_reader_settings">Текст</string>
|
||||
<string name="translator_reader_settings">Перекладач</string>
|
||||
<string name="theme_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_desc">Перевіряти на новішу версію застосунку при запуску</string>
|
||||
<string name="side_padding_option">Бокові відступи</string>
|
||||
<string name="translator_language_option">Мова перекладача</string>
|
||||
<string name="translator_enable_default_option">Увімкнути перекладач</string>
|
||||
<string name="translator_enable_default_option_desc">Увімкнути перекладач при додаванні книги за замовчуванням</string>
|
||||
<string name="translator_double_click_to_translate_option">Переклад подвійним кліком</string>
|
||||
<string name="translator_double_click_to_translate_option_desc">Двічі клацніть на абзац, щоб перекласти його</string>
|
||||
<string name="translator_translate_on_start_option">Перекладати при відкритті книги</string>
|
||||
<string name="translator_translate_on_start_option_desc">Автоматично перекладати весь текст при відкритті книги</string>
|
||||
|
||||
<!-- Colors -->
|
||||
<string name="red_color">Червоний</string>
|
||||
|
|
@ -255,7 +288,12 @@
|
|||
<string name="book_moved">Ця книга була успішно переміщена.</string>
|
||||
<string name="copied">Скопійовано</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 -->
|
||||
<string name="history_element_deleted">Елемент історії був успішно видалений.</string>
|
||||
|
|
@ -282,14 +320,25 @@
|
|||
Дякуємо за те, що прочитали \"%1$s\" з нами.
|
||||
Ми сподіваємось, що вам сподобалася ця книга!
|
||||
</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 -->
|
||||
<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_2">Натисніть, щоб перевірити на оновлення</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="licenses_option">Ліцензії з відкритим кодом</string>
|
||||
<string name="credits_option">Заслуги</string>
|
||||
|
|
@ -474,6 +523,8 @@
|
|||
<string name="reset_start_content_desc">Скинути початковий посібник</string>
|
||||
<string name="hide_edit_title_content_desc">Вийти з редагування назви</string>
|
||||
<string name="open_in_web_content_desc">Відкрити в браузері</string>
|
||||
|
||||
<string name="translator_content_desc">Перекладач</string>
|
||||
<string name="translator_language_content_desc">Поміняти мови місцями</string>
|
||||
<string name="downloaded_content_desc">Завантажено</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -55,6 +55,8 @@
|
|||
<string name="move_book">Move book?</string>
|
||||
<string name="delete_history">Delete reading history?</string>
|
||||
<string name="confirm_update">Confirm update?</string>
|
||||
<string name="download_language">Download %1$s?</string>
|
||||
<string name="download_languages">Download languages?</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
|
|
@ -97,6 +99,17 @@
|
|||
By clicking the «Download» button you will be transferred
|
||||
to your browser and download the latest update automatically.
|
||||
</string>
|
||||
<string name="download_language_description">
|
||||
You need to download this language before using it.
|
||||
After downloading, it will be available offline and will not require internet.
|
||||
Each language is about ~30mb in size, it is recommended to use Wi-Fi to download it.
|
||||
</string>
|
||||
<string name="download_languages_description">
|
||||
You need to download these languages before.
|
||||
Each language is about ~30mb in size, it is recommended to use Wi-Fi to download it.
|
||||
You can select which languages you want to download,
|
||||
all unselected languages will not be translated.
|
||||
</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
|
|
@ -123,6 +136,12 @@
|
|||
Something went wrong.
|
||||
Try again later.
|
||||
</string>
|
||||
<string name="error_language_not_identified">Could not identify language.</string>
|
||||
<string name="error_languages_not_identified">Could not identify any languages.</string>
|
||||
<string name="error_cannot_select_language">Cannot select same language twice.</string>
|
||||
<string name="error_wrong_language">Cannot select this language.</string>
|
||||
<string name="error_could_not_download_language">Could not download language.</string>
|
||||
<string name="error_could_not_download_languages">Could not download languages.</string>
|
||||
|
||||
<!-- Is Empty messages -->
|
||||
<string name="browse_empty">
|
||||
|
|
@ -134,6 +153,9 @@
|
|||
<string name="history_empty">
|
||||
Let your last book you read open the pages of a new great story of your reading journey
|
||||
</string>
|
||||
<string name="translator_languages_empty">
|
||||
There is no such language with this name, or it is not supported
|
||||
</string>
|
||||
|
||||
<!-- Query strings -->
|
||||
<string name="selected_items_count_query">%1$s selected</string>
|
||||
|
|
@ -168,11 +190,14 @@
|
|||
<string name="no">No</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="undo_translation">Undo</string>
|
||||
<string name="continue_download">Continue</string>
|
||||
|
||||
<!-- Items -->
|
||||
<string name="files">files</string>
|
||||
<string name="books">books</string>
|
||||
<string name="history">history</string>
|
||||
<string name="languages">languages</string>
|
||||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Unknown</string>
|
||||
|
|
@ -199,13 +224,14 @@
|
|||
<string name="general_settings">General</string>
|
||||
<string name="general_settings_desc">App language</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_desc">Font settings, text settings</string>
|
||||
<string name="reader_settings_desc">Translator, font, text</string>
|
||||
|
||||
<!-- Settings subcategories -->
|
||||
<string name="font_reader_settings">Font</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="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_desc">Check for newer app version on startup</string>
|
||||
<string name="side_padding_option">Side padding</string>
|
||||
<string name="translator_language_option">Translator language</string>
|
||||
<string name="translator_enable_default_option">Enable translator</string>
|
||||
<string name="translator_enable_default_option_desc">Enable translator when adding a book by default</string>
|
||||
<string name="translator_double_click_to_translate_option">Double click translation</string>
|
||||
<string name="translator_double_click_to_translate_option_desc">Double click on paragraph to translate it</string>
|
||||
<string name="translator_translate_on_start_option">Translate when open a book</string>
|
||||
<string name="translator_translate_on_start_option_desc">Automatically translate the whole text when open a book</string>
|
||||
|
||||
<!-- Colors -->
|
||||
<string name="red_color">Red</string>
|
||||
|
|
@ -274,6 +307,13 @@
|
|||
<string name="copied">Copied</string>
|
||||
<string name="history_deleted">History was successfully deleted.</string>
|
||||
<string name="no_updates">The latest version of the app is already installed.</string>
|
||||
<string name="language_downloaded">Language was successfully downloaded and selected.</string>
|
||||
<string name="languages_downloaded">
|
||||
All required languages are downloaded. Translation will start soon.
|
||||
</string>
|
||||
<string name="translator_canceling">Translation is canceling. Please wait.</string>
|
||||
<string name="translator_text_canceling">Translated text is canceling. Please wait.</string>
|
||||
<string name="translator_starting">Translation will start soon. Please wait.</string>
|
||||
|
||||
<!-- Snackbar messages -->
|
||||
<string name="history_element_deleted">History element was successfully deleted.</string>
|
||||
|
|
@ -302,6 +342,17 @@
|
|||
</string>
|
||||
<string name="letters_and_words">%1$s characters, %2$s words.</string>
|
||||
|
||||
<!-- Translator -->
|
||||
<string name="translator_auto">Detect language</string>
|
||||
<string name="translator_auto_short">Detect</string>
|
||||
<string name="translator_query">Translate %1$s</string>
|
||||
<string name="translator_from">from</string>
|
||||
<string name="translator_to">to</string>
|
||||
<string name="translator_all_languages">All languages</string>
|
||||
<string name="translator_recent_languages">Recent languages</string>
|
||||
<string name="translator_occurrences">%1$s paragraph(s)</string>
|
||||
<string name="translator_show_original">Show original (%1$s to %2$s)</string>
|
||||
|
||||
<!-- About -->
|
||||
<string name="app_version_option">App version</string>
|
||||
<string name="app_version_option_desc_1">Version v%1$s</string>
|
||||
|
|
@ -493,5 +544,8 @@
|
|||
<string name="reset_start_content_desc">Reset start guide</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="translator_content_desc">Translator</string>
|
||||
<string name="translator_language_content_desc">Change language places</string>
|
||||
<string name="downloaded_content_desc">Downloaded</string>
|
||||
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue