⚖️ Merge changes from "development"

Merge changes from another branch.
This commit is contained in:
Acclorite 2024-08-15 14:01:05 +03:00
commit 6ceb27254f
79 changed files with 512 additions and 490 deletions

View file

@ -39,12 +39,14 @@ Built using Jetpack Compose and Kotlin.
<img src="https://github.com/Acclorite/book-story/blob/master/assets/readme/book's-story.gif" width="35%"> <img src="https://github.com/Acclorite/book-story/blob/master/assets/readme/book's-story.gif" width="35%">
<div> <div>
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Library Dark + Frame.png" width="30%" /> <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Book-Info Dark + Frame.png" width="30%" /> <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Reader Dark + Frame.png" width="30%" /> <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Library Light + Frame.png" width="30%" /> <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Book-Info Light + Frame.png" width="30%" /> <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="30%" />
<img src="https://github.com/Acclorite/book-story/blob/master/assets/Reader Light + Frame.png" width="30%" /> <img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png" width="30%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="45%" />
<img src="/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png" width="45%" />
</div> </div>
</br> </br>
@ -117,6 +119,10 @@ _Don't see your favorite app store? Create an [Issue](https://github.com/Acclori
## 🤝 How to contribute ## 🤝 How to contribute
- **The most improtant thing is I do not want someone's code.** This project is maintaining only by me and I want it to be this way. Creating Pull Requests will not have much sense as they will probably be closed. - **The most improtant thing is I do not want someone's code.** This project is maintaining only by me and I want it to be this way. Creating Pull Requests will not have much sense as they will probably be closed.
- **Place your suggestions on Issues
with [Suggestion Needed](https://github.com/Acclorite/book-story/labels/%F0%9F%93%9D%20Suggestion%20needed)
label.** When the issue has this label that means I seek new ideas on how to improve what I
already propose. Feel free to tell me what's on your mind!
- **Share your ideas and suggestions.** Your ideas and suggestions are always welcome, if you miss some feature or got an interesting idea, feel free to create a new [Issue](https://github.com/Acclorite/book-story/issues). - **Share your ideas and suggestions.** Your ideas and suggestions are always welcome, if you miss some feature or got an interesting idea, feel free to create a new [Issue](https://github.com/Acclorite/book-story/issues).
- **Report bugs**. Had a crash or something went wrong? Create a new [Issue](https://github.com/Acclorite/book-story/issues), the more information you can give the better it is. - **Report bugs**. Had a crash or something went wrong? Create a new [Issue](https://github.com/Acclorite/book-story/issues), the more information you can give the better it is.
- **Enjoy the app**. Your best contribution is using my app and enjoying it! - **Enjoy the app**. Your best contribution is using my app and enjoying it!

View file

@ -137,9 +137,6 @@ dependencies {
implementation("com.tom-roush:pdfbox-android:2.0.27.0") implementation("com.tom-roush:pdfbox-android:2.0.27.0")
// Epub parser // Epub parser
implementation("com.positiondev.epublib:epublib-core:3.1") {
exclude("xmlpull")
}
implementation("org.jsoup:jsoup:1.18.1") implementation("org.jsoup:jsoup:1.18.1")
// Fb2 parser // Fb2 parser

View file

@ -37,11 +37,12 @@
-dontwarn org.xmlpull.v1.XmlPullParserFactory -dontwarn org.xmlpull.v1.XmlPullParserFactory
# Fixes issue with displaying .xml files, but may produce reproducibility issues # Fixes issue with displaying .xml files, but may produce reproducibility issues
-keep class org.xmlpull.** { *; } #-keep class org.xmlpull.** { *; }
-keepclassmembers class org.xmlpull.** { *; } #-keepclassmembers class org.xmlpull.** { *; }
# Keeping names of classes that produce undeterministic results # Keeping names of classes that produce undeterministic results
-keepnames class ** -keepnames class **
-keepnames class org.xmlpull.** { *; } -keepnames class org.xmlpull.** { *; }
-keepclassmembernames class org.xmlpull.** { *; } -keepclassmembernames class org.xmlpull.** { *; }
-keepnames class kotlin.reflect.jvm.internal.impl.builtins.PrimitiveType { values(); } -keepnames class kotlin.reflect.jvm.internal.impl.builtins.PrimitiveType { values(); }
-keepnames class * implements android.os.Parcelable { ** CREATOR; }

View file

@ -17,26 +17,6 @@
android:name="ua.acclorite.book_story.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:name="ua.acclorite.book_story.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
tools:node="remove" /> tools:node="remove" />
<queries>
<!-- Translator -->
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<!-- Browser -->
<intent>
<action android:name="android.intent.action.WEB_SEARCH" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
</intent>
</queries>
<application <application
android:name=".Application" android:name=".Application"
android:allowBackup="false" android:allowBackup="false"

View file

@ -1,16 +1,17 @@
package ua.acclorite.book_story.data.parser.epub package ua.acclorite.book_story.data.parser.epub
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader import org.jsoup.Jsoup
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.File import java.io.File
import java.io.FileInputStream import java.util.zip.ZipFile
import javax.inject.Inject import javax.inject.Inject
class EpubFileParser @Inject constructor() : FileParser { class EpubFileParser @Inject constructor() : FileParser {
@ -21,45 +22,81 @@ class EpubFileParser @Inject constructor() : FileParser {
} }
try { try {
val epubReader = EpubReader() var book: Pair<Book, CoverImage?>? = null
val book = withContext(Dispatchers.IO) {
FileInputStream(file).use { withContext(Dispatchers.IO) {
epubReader.readEpub(it) ZipFile(file).use { zip ->
val opfEntry = zip.entries().asSequence().find { entry ->
entry.name.endsWith(".opf")
} ?: return@withContext
val opfContent = zip
.getInputStream(opfEntry)
.bufferedReader()
.use { it.readText() }
val document = Jsoup.parse(opfContent)
val title = document.select("metadata > dc|title").text().trim()
val author = UIText.StringValue(
document.select("metadata > dc|creator").text().trim()
)
val description = Jsoup.parse(
document.select("metadata > dc|description").text()
).text()
var coverImagePath: String? = null
val coverId = document
.select("metadata > meta[name=cover]")
.attr("content")
if (coverId.isNotBlank()) {
coverImagePath = document
.select("manifest > item[id=$coverId]")
.attr("href")
}
if (coverImagePath.isNullOrBlank()) {
coverImagePath = document
.select("manifest > item[media-type*=image]")
.firstOrNull()?.attr("href")
}
book = Book(
title = title,
author = author,
description = description,
textPath = "",
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
coverImage = null
) to extractCoverImageBitmap(file, coverImagePath)
} }
} }
return book
val metadata = book.metadata
val author = UIText.StringValue(metadata.authors.joinToString(", "))
val title = metadata.titles.joinToString(", ")
val coverImage = book.coverImage?.let { BitmapFactory.decodeStream(it.inputStream) }
var description: StringBuilder? = StringBuilder()
metadata.descriptions.forEach {
description?.append(it)?.append(" ")
}
if (description != null) {
if (description.isBlank()) {
description = null
}
}
return Book(
title = title,
author = author,
description = description?.toString(),
textPath = "",
scrollIndex = 0,
scrollOffset = 0,
progress = 0f,
filePath = file.path,
lastOpened = null,
category = Category.entries[0],
coverImage = null
) to coverImage
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return null return null
} }
} }
} }
private fun extractCoverImageBitmap(file: File, coverImagePath: String?): Bitmap? {
if (coverImagePath.isNullOrBlank()) {
return null
}
ZipFile(file).use { zip ->
zip.entries().asSequence().forEach { entry ->
if (entry.name.endsWith(coverImagePath)) {
val imageBytes = zip.getInputStream(entry).readBytes()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
}
}
return null
}

View file

@ -2,19 +2,13 @@ package ua.acclorite.book_story.data.parser.epub
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.siegmann.epublib.epub.EpubReader
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document.OutputSettings
import org.jsoup.safety.Safelist
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileInputStream import java.util.zip.ZipFile
import java.io.InputStreamReader
import java.nio.charset.Charset
import javax.inject.Inject import javax.inject.Inject
@ -26,66 +20,42 @@ class EpubTextParser @Inject constructor() : TextParser {
} }
try { try {
val book = withContext(Dispatchers.IO) { val lines = mutableListOf<String>()
EpubReader().readEpub(FileInputStream(file))
}
val unformattedText = StringBuilder()
for (spineReference in book.spine.spineReferences) { withContext(Dispatchers.IO) {
val resource = spineReference.resource ZipFile(file).use { zip ->
val inputStream = resource.inputStream zip.entries().asSequence().forEach { entry ->
val reader = BufferedReader( if (
InputStreamReader( entry.name.endsWith(".xhtml")
inputStream, || entry.name.endsWith(".html")
Charset.forName("UTF-8") || entry.name.endsWith(".xml")
) || entry.name.endsWith(".htm")
) ) {
var line: String? val content = zip.getInputStream(entry).bufferedReader()
.use {
it.readText()
}
withContext(Dispatchers.IO) { val document = Jsoup.parse(content)
inputStream.close() document.select("p").append("\n")
} document
.wholeText()
while (withContext(Dispatchers.IO) { reader.readLine() } .lines()
.also { line = it } != null) { .forEach { line ->
unformattedText.append(line).append("\n") if (line.isNotBlank()) {
} lines.add(line.trim())
}
withContext(Dispatchers.IO) { }
reader.close() }
}
}
val strings = mutableListOf<String>()
val parsedText = Jsoup.parse(unformattedText.toString())
parsedText.outputSettings(OutputSettings().prettyPrint(false))
parsedText.select("br").append("\n")
parsedText.select("p").prepend("\n")
parsedText.select("em").append(" ").prepend("")
val formattedText = Jsoup.clean(
parsedText.html(),
"",
Safelist.none(),
OutputSettings().prettyPrint(false)
)
formattedText
.replace("&nbsp;", "")
.replace("\u00a0", "")
.split("\n")
.forEach {
if (it.isNotBlank()) {
strings.add(it.trim())
} }
} }
}
if (strings.isEmpty()) { if (lines.isEmpty()) {
return Resource.Error(UIText.StringResource(R.string.error_file_empty)) return Resource.Error(UIText.StringResource(R.string.error_file_empty))
} }
return Resource.Success(strings) return Resource.Success(lines)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return Resource.Error( return Resource.Error(

View file

@ -58,20 +58,6 @@ class Fb2TextParser @Inject constructor() : TextParser {
return@forEachIndexed return@forEachIndexed
} }
if (
line.all {
if (it == ' ') {
true
} else if (it.isUpperCase() || it.isDigit() || it == '-') {
true
} else {
false
}
}
) {
return@forEachIndexed
}
if (line.all { it.isDigit() }) { if (line.all { it.isDigit() }) {
return@forEachIndexed return@forEachIndexed
} }

View file

@ -1,15 +1,11 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ReaderLine
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.ID
import javax.inject.Inject import javax.inject.Inject
class GetText @Inject constructor(private val repository: BookRepository) { class GetText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(textPath: String): Map<ID, ReaderLine> { suspend fun execute(textPath: String): List<String> {
return repository.getBookText(textPath = textPath).withIndex().associate { return repository.getBookText(textPath = textPath)
it.index to ReaderLine(line = it.value)
}
} }
} }

View file

@ -20,12 +20,14 @@ import ua.acclorite.book_story.R
/** /**
* Custom Checkbox. Has a Circle shape. * Custom Checkbox. Has a Circle shape.
* *
* @param modifier Modifier.
* @param selected Whether checked or not. * @param selected Whether checked or not.
* @param containerColor Container color. * @param containerColor Container color.
* @param size Size of the checkbox icon(not the whole checkbox). * @param size Size of the checkbox icon(not the whole checkbox).
*/ */
@Composable @Composable
fun CustomCheckbox( fun CustomCheckbox(
modifier: Modifier = Modifier,
selected: Boolean, selected: Boolean,
containerColor: Color, containerColor: Color,
size: Dp = 22.dp size: Dp = 22.dp
@ -33,7 +35,7 @@ fun CustomCheckbox(
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
tint = if (selected) containerColor else Color.Transparent, tint = if (selected) containerColor else Color.Transparent,
modifier = Modifier modifier = modifier
.border( .border(
width = if (!selected) 1.dp else 0.dp, width = if (!selected) 1.dp else 0.dp,
if (!selected) MaterialTheme.colorScheme.outline else Color.Transparent, if (!selected) MaterialTheme.colorScheme.outline else Color.Transparent,

View file

@ -29,8 +29,10 @@ import ua.acclorite.book_story.R
private const val MENU_ITEM_COPY = 0 private const val MENU_ITEM_COPY = 0
private const val MENU_ITEM_TRANSLATE = 1 private const val MENU_ITEM_SHARE = 1
private const val MENU_ITEM_DICTIONARY = 2 private const val MENU_ITEM_WEB = 2
private const val MENU_ITEM_TRANSLATE = 3
private const val MENU_ITEM_DICTIONARY = 4
/** /**
* Custom Text ActionMode callback. Used in pair with [CustomSelectionToolbar]. Follow [TextToolbar] for more info. * Custom Text ActionMode callback. Used in pair with [CustomSelectionToolbar]. Follow [TextToolbar] for more info.
@ -39,6 +41,8 @@ private class CustomTextActionModeCallback(
private val context: Context, private val context: Context,
var rect: Rect = Rect.Zero, var rect: Rect = Rect.Zero,
var onCopyRequested: (() -> Unit)? = null, var onCopyRequested: (() -> Unit)? = null,
var onShareRequested: (() -> Unit)? = null,
var onWebSearchRequested: (() -> Unit)? = null,
var onTranslateRequested: (() -> Unit)? = null, var onTranslateRequested: (() -> Unit)? = null,
var onDictionaryRequested: (() -> Unit)? = null var onDictionaryRequested: (() -> Unit)? = null
) : ActionMode.Callback { ) : ActionMode.Callback {
@ -51,13 +55,23 @@ private class CustomTextActionModeCallback(
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
} }
onShareRequested?.let {
menu.add(0, MENU_ITEM_SHARE, 1, context.getString(R.string.share))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onWebSearchRequested?.let {
menu.add(0, MENU_ITEM_WEB, 2, context.getString(R.string.web_search))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
onTranslateRequested?.let { onTranslateRequested?.let {
menu.add(0, MENU_ITEM_TRANSLATE, 1, context.getString(R.string.translate)) menu.add(0, MENU_ITEM_TRANSLATE, 3, context.getString(R.string.translate))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
} }
onDictionaryRequested?.let { onDictionaryRequested?.let {
menu.add(0, MENU_ITEM_DICTIONARY, 2, context.getString(R.string.dictionary)) menu.add(0, MENU_ITEM_DICTIONARY, 4, context.getString(R.string.dictionary))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
} }
@ -71,6 +85,8 @@ private class CustomTextActionModeCallback(
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
when (item!!.itemId) { when (item!!.itemId) {
MENU_ITEM_COPY -> onCopyRequested?.invoke() MENU_ITEM_COPY -> onCopyRequested?.invoke()
MENU_ITEM_SHARE -> onShareRequested?.invoke()
MENU_ITEM_WEB -> onWebSearchRequested?.invoke()
MENU_ITEM_TRANSLATE -> onTranslateRequested?.invoke() MENU_ITEM_TRANSLATE -> onTranslateRequested?.invoke()
MENU_ITEM_DICTIONARY -> onDictionaryRequested?.invoke() MENU_ITEM_DICTIONARY -> onDictionaryRequested?.invoke()
else -> return false else -> return false
@ -116,12 +132,15 @@ private class FloatingTextActionModeCallback(
} }
/** /**
* Custom Selection Toolbar. Used in pair with [CustomSelectionContainer] to display custom toolbar. * Custom Selection Toolbar.
* Used in pair with [CustomSelectionContainer] to display custom toolbar.
*/ */
private class CustomSelectionToolbar( private class CustomSelectionToolbar(
private val view: View, private val view: View,
context: Context, context: Context,
private val onCopyRequest: (() -> Unit)?, private val onCopyRequest: (() -> Unit)?,
private val onShareRequest: ((String) -> Unit)?,
private val onWebSearchRequest: ((String) -> Unit)?,
private val onTranslateRequest: ((String) -> Unit)?, private val onTranslateRequest: ((String) -> Unit)?,
private val onDictionaryRequest: ((String) -> Unit)? private val onDictionaryRequest: ((String) -> Unit)?
) : TextToolbar { ) : TextToolbar {
@ -145,6 +164,36 @@ private class CustomSelectionToolbar(
onCopyRequested?.invoke() onCopyRequested?.invoke()
onCopyRequest?.invoke() onCopyRequest?.invoke()
} }
callback.onShareRequested = {
val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke()
val currentClipboard = clipboardManager.text
onShareRequest?.invoke(currentClipboard.toString())
if (previousClipboard != null) {
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}
callback.onWebSearchRequested = {
val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke()
val currentClipboard = clipboardManager.text
onWebSearchRequest?.invoke(currentClipboard.toString())
if (previousClipboard != null) {
clipboardManager.setPrimaryClip(
previousClipboard
)
} else {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
}
}
callback.onTranslateRequested = { callback.onTranslateRequested = {
val previousClipboard = clipboardManager.primaryClip val previousClipboard = clipboardManager.primaryClip
onCopyRequested?.invoke() onCopyRequested?.invoke()
@ -205,6 +254,8 @@ private class CustomSelectionToolbar(
@Composable @Composable
fun CustomSelectionContainer( fun CustomSelectionContainer(
onCopyRequested: (() -> Unit), onCopyRequested: (() -> Unit),
onShareRequested: ((String) -> Unit),
onWebSearchRequested: ((String) -> Unit),
onTranslateRequested: ((String) -> Unit), onTranslateRequested: ((String) -> Unit),
onDictionaryRequested: ((String) -> Unit), onDictionaryRequested: ((String) -> Unit),
content: @Composable (toolbarHidden: Boolean) -> Unit content: @Composable (toolbarHidden: Boolean) -> Unit
@ -220,6 +271,12 @@ fun CustomSelectionContainer(
onCopyRequest = { onCopyRequest = {
onCopyRequested() onCopyRequested()
}, },
onShareRequest = {
onShareRequested(it)
},
onWebSearchRequest = {
onWebSearchRequested(it)
},
onTranslateRequest = { onTranslateRequest = {
onTranslateRequested(it) onTranslateRequested(it)
}, },

View file

@ -60,7 +60,7 @@ fun CustomDialogWithContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
drawableIcon: Painter? = null, drawableIcon: Painter? = null,
imageVectorIcon: ImageVector? = null, imageVectorIcon: ImageVector? = null,
backgroundTransparency: Float = 0.3f, backgroundTransparency: Float = 0.5f,
title: String, title: String,
description: String?, description: String?,
actionText: String?, actionText: String?,

View file

@ -62,7 +62,7 @@ fun CustomDialogWithLazyColumn(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
drawableIcon: Painter? = null, drawableIcon: Painter? = null,
imageVectorIcon: ImageVector? = null, imageVectorIcon: ImageVector? = null,
backgroundTransparency: Float = 0.3f, backgroundTransparency: Float = 0.5f,
title: String, title: String,
description: String?, description: String?,
actionText: String?, actionText: String?,

View file

@ -15,16 +15,18 @@ fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",",
fun Intent.launchActivity( fun Intent.launchActivity(
activity: ComponentActivity, activity: ComponentActivity,
createChooser: Boolean = false,
openInNewWindow: Boolean = true, openInNewWindow: Boolean = true,
success: (() -> Unit)? = null, success: (() -> Unit)? = null,
error: () -> Unit error: () -> Unit
) { ) {
val intent = if (createChooser) Intent.createChooser(this, "") else this
if (openInNewWindow) { if (openInNewWindow) {
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
try { try {
activity.baseContext.startActivity(this) activity.baseContext.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
error() error()
return return

View file

@ -90,7 +90,9 @@ fun LicenseItem(
Text( Text(
text = library.author, text = library.author,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
} }
if (library.licenses.isNotEmpty()) { if (library.licenses.isNotEmpty()) {

View file

@ -534,7 +534,7 @@ class BookInfoViewModel @Inject constructor(
val updatedText = updatedBook.text val updatedText = updatedBook.text
val text = getText.execute(book.textPath) val text = getText.execute(book.textPath)
if (updatedText != text.map { it.value.line }) { if (updatedText != text) {
textUpdated = true textUpdated = true
} }

View file

@ -64,7 +64,7 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
Spacer(modifier = Modifier.width(24.dp)) Spacer(modifier = Modifier.width(24.dp))
CustomCheckbox( CustomCheckbox(
selected = result.second, selected = result.second,
MaterialTheme.colorScheme.surfaceContainerHigh containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
) )
} }
} }

View file

@ -52,7 +52,7 @@ import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.OnNavigate import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomSelectionContainer import ua.acclorite.book_story.presentation.components.CustomSelectionContainer
import ua.acclorite.book_story.presentation.components.customItems import ua.acclorite.book_story.presentation.components.customItemsIndexed
import ua.acclorite.book_story.presentation.components.is_messages.IsError import ua.acclorite.book_story.presentation.components.is_messages.IsError
import ua.acclorite.book_story.presentation.data.LocalNavigator import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
@ -188,9 +188,6 @@ private fun ReaderScreen(
} }
} }
val text = remember(state.value.text) {
state.value.text.toList().sortedBy { it.first }
}
val fontFamily = remember(mainState.value.fontFamily) { val fontFamily = remember(mainState.value.fontFamily) {
Constants.FONTS.find { Constants.FONTS.find {
it.id == mainState.value.fontFamily it.id == mainState.value.fontFamily
@ -277,10 +274,41 @@ private fun ReaderScreen(
).show() ).show()
} }
}, },
onShareRequested = { textToShare ->
onEvent(
ReaderEvent.OnOpenShareApp(
textToShare = textToShare,
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_share_app),
Toast.LENGTH_SHORT
).show()
}
)
)
},
onWebSearchRequested = { textToSearch ->
onEvent(
ReaderEvent.OnOpenWebBrowser(
textToSearch = textToSearch,
context,
noAppsFound = {
Toast.makeText(
context,
context.getString(R.string.error_no_browser),
Toast.LENGTH_SHORT
).show()
}
)
)
},
onTranslateRequested = { textToTranslate -> onTranslateRequested = { textToTranslate ->
onEvent( onEvent(
ReaderEvent.OnOpenTranslator( ReaderEvent.OnOpenTranslator(
textToTranslate, textToTranslate = textToTranslate,
translateWholeParagraph = false,
context, context,
noAppsFound = { noAppsFound = {
Toast.makeText( Toast.makeText(
@ -300,7 +328,7 @@ private fun ReaderScreen(
noAppsFound = { noAppsFound = {
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.error_no_browser), context.getString(R.string.error_no_dictionary),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@ -359,11 +387,11 @@ private fun ReaderScreen(
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(18.dp))
} }
customItems( customItemsIndexed(
text, key = { key -> key.first } state.value.text, key = { _, index -> index }
) { line -> ) { _, line ->
ReaderTextParagraph( ReaderTextParagraph(
line = line.second, line = line,
context = context, context = context,
fontFamily = fontFamily, fontFamily = fontFamily,
fontColor = fontColor.value, fontColor = fontColor.value,

View file

@ -22,7 +22,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.FontWithName import ua.acclorite.book_story.domain.model.FontWithName
import ua.acclorite.book_story.domain.model.ReaderLine
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
/** /**
@ -44,7 +43,7 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LazyItemScope.ReaderTextParagraph( fun LazyItemScope.ReaderTextParagraph(
line: ReaderLine, line: String,
context: Context, context: Context,
fontFamily: FontWithName, fontFamily: FontWithName,
fontColor: Color, fontColor: Color,
@ -72,7 +71,7 @@ fun LazyItemScope.ReaderTextParagraph(
append(" ") append(" ")
} }
append(line.line) append(line)
}, },
modifier = Modifier.then( modifier = Modifier.then(
if ( if (
@ -85,7 +84,8 @@ fun LazyItemScope.ReaderTextParagraph(
onDoubleClick = { onDoubleClick = {
onEvent( onEvent(
ReaderEvent.OnOpenTranslator( ReaderEvent.OnOpenTranslator(
textToTranslate = line.line, textToTranslate = line,
translateWholeParagraph = true,
context = context as ComponentActivity, context = context as ComponentActivity,
noAppsFound = { noAppsFound = {
Toast.makeText( Toast.makeText(

View file

@ -46,6 +46,19 @@ sealed class ReaderEvent {
data class OnOpenTranslator( data class OnOpenTranslator(
val textToTranslate: String, val textToTranslate: String,
val translateWholeParagraph: Boolean,
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()
data class OnOpenShareApp(
val textToShare: String,
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()
data class OnOpenWebBrowser(
val textToSearch: String,
val context: ComponentActivity, val context: ComponentActivity,
val noAppsFound: () -> Unit val noAppsFound: () -> Unit
) : ReaderEvent() ) : ReaderEvent()

View file

@ -3,15 +3,13 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.ReaderLine
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.ID
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
data class ReaderState( data class ReaderState(
val book: Book = Constants.EMPTY_BOOK, val book: Book = Constants.EMPTY_BOOK,
val text: Map<ID, ReaderLine> = emptyMap(), val text: List<String> = emptyList(),
val words: Int = 0, val words: Int = 0,
val letters: Int = 0, val letters: Int = 0,
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),

View file

@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
@ -73,7 +74,6 @@ class ReaderViewModel @Inject constructor(
} }
val textAsLine = text val textAsLine = text
.map { it.value.line }
.joinToString( .joinToString(
separator = "\n" separator = "\n"
) )
@ -338,7 +338,10 @@ class ReaderViewModel @Inject constructor(
) )
var translatorFailure = false var translatorFailure = false
translatorIntent.launchActivity(event.context) { translatorIntent.launchActivity(
event.context,
createChooser = !event.translateWholeParagraph
) {
translatorFailure = true translatorFailure = true
} }
if (!translatorFailure) { if (!translatorFailure) {
@ -357,22 +360,85 @@ class ReaderViewModel @Inject constructor(
} }
} }
is ReaderEvent.OnOpenDictionary -> { is ReaderEvent.OnOpenShareApp -> {
launch {
val shareIntent = Intent()
shareIntent.action = Intent.ACTION_SEND
shareIntent.type = "text/plain"
shareIntent.putExtra(
Intent.EXTRA_SUBJECT,
event.context.getString(R.string.app_name)
)
shareIntent.putExtra(
Intent.EXTRA_TEXT,
event.textToShare.trim()
)
var shareFailure = false
shareIntent.launchActivity(event.context, createChooser = true) {
shareFailure = true
}
if (!shareFailure) {
return@launch
}
event.noAppsFound()
}
}
is ReaderEvent.OnOpenWebBrowser -> {
launch { launch {
val browserIntent = Intent() val browserIntent = Intent()
browserIntent.action = Intent.ACTION_WEB_SEARCH browserIntent.action = Intent.ACTION_WEB_SEARCH
browserIntent.putExtra( browserIntent.putExtra(
SearchManager.QUERY, SearchManager.QUERY,
"dictionary" + event.textToSearch
": ${event.textToDefine.trim()}"
) )
var failure = false var browserFailure = false
browserIntent.launchActivity(event.context) { browserIntent.launchActivity(event.context) {
failure = true browserFailure = true
} }
if (!failure) { if (!browserFailure) {
return@launch
}
event.noAppsFound()
}
}
is ReaderEvent.OnOpenDictionary -> {
launch {
val dictionaryIntent = Intent()
val browserIntent = Intent()
dictionaryIntent.type = "text/plain"
dictionaryIntent.action = Intent.ACTION_PROCESS_TEXT
dictionaryIntent.putExtra(
Intent.EXTRA_PROCESS_TEXT,
event.textToDefine.trim()
)
dictionaryIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
browserIntent.action = Intent.ACTION_VIEW
val text = event.textToDefine.trim().replace(" ", "+")
browserIntent.data = Uri.parse("https://www.onelook.com/?w=$text")
var dictionaryFailure = false
dictionaryIntent.launchActivity(event.context, createChooser = true) {
dictionaryFailure = true
}
if (!dictionaryFailure) {
return@launch
}
var browserFailure = false
browserIntent.launchActivity(event.context) {
browserFailure = true
}
if (!browserFailure) {
return@launch return@launch
} }

View file

@ -1,38 +1,70 @@
package ua.acclorite.book_story.presentation.screens.settings.components package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.SegmentedButton import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.Icon
import androidx.compose.material3.SegmentedButtonColors
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
/** /**
* Segmented Button with Title. Contains [buttons]. * Segmented Button with Title.
* Uses custom implementation(material3 one has many flaws).
* Contains [buttons].
*
* @param modifier Modifier.
* @param title Title of the buttons.
* @param buttons [ButtonItem]s.
* @param enabled Whether button is enabled.
* @param horizontalPadding Horizontal item padding.
* @param verticalPadding Vertical item padding.
* @param onClick OnClick callback.
*/ */
@Composable @Composable
fun SegmentedButtonWithTitle( fun SegmentedButtonWithTitle(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String, title: String,
buttons: List<ButtonItem>, buttons: List<ButtonItem>,
locked: Boolean, enabled: Boolean = true,
horizontalPadding: Dp = 18.dp, horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp, verticalPadding: Dp = 8.dp,
onClick: (ButtonItem) -> Unit onClick: (ButtonItem) -> Unit
) { ) {
val colors = SegmentedButtonDefaults.colors()
Column( Column(
modifier modifier
.fillMaxWidth() .fillMaxWidth()
@ -42,50 +74,118 @@ fun SegmentedButtonWithTitle(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
SingleChoiceSegmentedButtonRow { LazyRow(Modifier.fillMaxWidth()) {
buttons.forEachIndexed { index, buttonItem -> item {
SegmentedButton( Row(
enabled = locked, Modifier
selected = buttonItem.selected, .clip(CircleShape)
onClick = { onClick(buttonItem) }, .border(
shape = when (index) { width = 0.5.dp,
buttons.lastIndex -> RoundedCornerShape( color = SegmentedButtonDefaults.colors().activeBorderColor,
topEndPercent = 100, shape = CircleShape
bottomEndPercent = 100
) )
.padding(0.5.dp)
) {
buttons.forEachIndexed { index, buttonItem ->
SegmentedButton(
button = buttonItem,
enabled = enabled,
shape = when (index) {
buttons.lastIndex -> RoundedCornerShape(
topEndPercent = 100,
bottomEndPercent = 100
)
0 -> RoundedCornerShape( 0 -> RoundedCornerShape(
topStartPercent = 100, topStartPercent = 100,
bottomStartPercent = 100 bottomStartPercent = 100
) )
else -> RoundedCornerShape(0) else -> RoundedCornerShape(0)
}, },
colors = SegmentedButtonDefaults.colors( onClick = { onClick(buttonItem) }
disabledInactiveBorderColor = colors.activeBorderColor,
disabledInactiveContentColor = colors.inactiveContentColor,
disabledActiveContainerColor = colors.activeContainerColor,
disabledActiveContentColor = colors.activeContentColor,
disabledInactiveContainerColor = colors.inactiveContainerColor,
disabledActiveBorderColor = colors.activeBorderColor,
),
label = {
Text(
text = buttonItem.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
} }
) }
} }
} }
} }
} }
/**
* Custom Segmented button.
* Adjusts width based on component name.
*
* @param button [ButtonItem].
* @param enabled Whether can be clicked.
* @param shape Shape of the button. For proper implementation should have 100% corners on first and last buttons(start and end).
* @param colors [SegmentedButtonColors].
* @param onClick OnClick callback.
*/
@Composable
private fun SegmentedButton(
button: ButtonItem,
enabled: Boolean,
shape: RoundedCornerShape,
colors: SegmentedButtonColors = SegmentedButtonDefaults.colors(),
onClick: () -> Unit
) {
Row(
modifier = Modifier
.height(40.dp)
.clip(shape)
.clickable(enabled = enabled && !button.selected) {
onClick()
}
.border(
width = 0.5.dp,
color = colors.activeBorderColor,
shape = shape
)
.padding(0.5.dp)
.background(
if (button.selected) colors.activeContainerColor
else Color.Transparent,
shape = shape
)
.padding(horizontal = 18.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAnimatedVisibility(
visible = button.selected,
enter = expandHorizontally()
+ slideInVertically(initialOffsetY = { it / 2 })
+ scaleIn()
+ fadeIn(),
exit = shrinkHorizontally()
+ slideOutVertically(targetOffsetY = { it / 2 })
+ scaleOut()
+ fadeOut()
) {
Row {
Icon(
imageVector = Icons.Default.Done,
contentDescription = stringResource(id = R.string.selected_content_desc),
modifier = Modifier
.size(18.dp),
tint = colors.activeContentColor
)
Spacer(modifier = Modifier.width(8.dp))
}
}
Text(
text = button.title,
style = button.textStyle,
fontWeight = FontWeight.Medium,
color = if (button.selected) colors.activeContentColor
else colors.inactiveContentColor
)
}
}

View file

@ -25,17 +25,16 @@ fun LazyItemScope.DarkThemeSetting(
SegmentedButtonWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.dark_theme_option), title = stringResource(id = R.string.dark_theme_option),
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
locked = true,
buttons = DarkTheme.entries.map { buttons = DarkTheme.entries.map {
ButtonItem( ButtonItem(
it.toString(), it.toString(),
when (it) { title = when (it) {
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off) DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
DarkTheme.ON -> stringResource(id = R.string.dark_theme_on) DarkTheme.ON -> stringResource(id = R.string.dark_theme_on)
DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system) DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system)
}, },
MaterialTheme.typography.labelLarge, textStyle = MaterialTheme.typography.labelLarge,
it == state.value.darkTheme selected = it == state.value.darkTheme
) )
} }
) { ) {

View file

@ -33,17 +33,16 @@ fun LazyItemScope.PureDarkSetting(
) { ) {
SegmentedButtonWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.pure_dark_option), title = stringResource(id = R.string.pure_dark_option),
locked = true,
buttons = PureDark.entries.map { buttons = PureDark.entries.map {
ButtonItem( ButtonItem(
it.toString(), id = it.toString(),
when (it) { title = when (it) {
PureDark.OFF -> stringResource(id = R.string.pure_dark_off) PureDark.OFF -> stringResource(id = R.string.pure_dark_off)
PureDark.ON -> stringResource(id = R.string.pure_dark_on) PureDark.ON -> stringResource(id = R.string.pure_dark_on)
PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver) PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver)
}, },
MaterialTheme.typography.labelLarge, textStyle = MaterialTheme.typography.labelLarge,
it == state.value.pureDark selected = it == state.value.pureDark
) )
} }
) { ) {

View file

@ -54,17 +54,17 @@ fun LazyItemScope.ThemeContrastSetting(
) { ) {
SegmentedButtonWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.theme_contrast_option), title = stringResource(id = R.string.theme_contrast_option),
locked = state.value.theme != Theme.DYNAMIC, enabled = state.value.theme != Theme.DYNAMIC,
buttons = ThemeContrast.entries.map { buttons = ThemeContrast.entries.map {
ButtonItem( ButtonItem(
it.toString(), id = it.toString(),
when (it) { title = when (it) {
ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard) ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard)
ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium) ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium)
ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high) ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
}, },
MaterialTheme.typography.labelLarge, textStyle = MaterialTheme.typography.labelLarge,
it == state.value.themeContrast selected = it == state.value.themeContrast
) )
} }
) { ) {

View file

@ -35,12 +35,12 @@ fun LazyItemScope.FontFamilySetting(
chips = Constants.FONTS chips = Constants.FONTS
.map { .map {
ButtonItem( ButtonItem(
it.id, id = it.id,
it.fontName.asString(), title = it.fontName.asString(),
MaterialTheme.typography.labelLarge.copy( textStyle = MaterialTheme.typography.labelLarge.copy(
fontFamily = it.font fontFamily = it.font
), ),
it.id == fontFamily.id selected = it.id == fontFamily.id
) )
}, },
onClick = { onClick = {

View file

@ -13,7 +13,7 @@ import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle
/** /**
* Font Style setting. * Font Style setting.
@ -30,27 +30,27 @@ fun LazyItemScope.FontStyleSetting(
} ?: Constants.FONTS[0] } ?: Constants.FONTS[0]
} }
ChipsWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.font_style_option), title = stringResource(id = R.string.font_style_option),
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
chips = listOf( buttons = listOf(
ButtonItem( ButtonItem(
"normal", id = "normal",
stringResource(id = R.string.font_style_normal), title = stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy( textStyle = MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal fontStyle = FontStyle.Normal
), ),
!state.value.isItalic!! selected = !state.value.isItalic!!
), ),
ButtonItem( ButtonItem(
"italic", id = "italic",
stringResource(id = R.string.font_style_italic), title = stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy( textStyle = MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font, fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
), ),
state.value.isItalic!! selected = state.value.isItalic!!
), ),
), ),
onClick = { onClick = {

View file

@ -64,10 +64,10 @@ fun StartSettings(
val languages = remember(mainState.value.language) { val languages = remember(mainState.value.language) {
Constants.LANGUAGES.sortedBy { it.second }.map { Constants.LANGUAGES.sortedBy { it.second }.map {
ButtonItem( ButtonItem(
it.first, id = it.first,
it.second, title = it.second,
TextStyle(), textStyle = TextStyle(),
it.first == mainState.value.language selected = it.first == mainState.value.language
) )
}.sortedBy { it.title } }.sortedBy { it.title }
} }

View file

@ -58,22 +58,6 @@ fun DefaultTransition(
} }
} }
@Composable
fun <T> T.DefaultTransition(
visible: Boolean,
modifier: Modifier = Modifier,
content: @Composable T.() -> Unit
) {
CustomAnimatedVisibility(
visible = visible,
modifier = modifier,
enter = Transitions.DefaultTransitionIn,
exit = Transitions.DefaultTransitionOut
) {
content()
}
}
@Composable @Composable
fun SlidingTransition( fun SlidingTransition(
visible: Boolean, visible: Boolean,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m260,700 l300,-140 140,-300 -300,140 -140,300ZM480,520q-17,0 -28.5,-11.5T440,480q0,-17 11.5,-28.5T480,440q17,0 28.5,11.5T520,480q0,17 -11.5,28.5T480,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m260,700 l300,-140 140,-300 -300,140 -140,300ZM480,520q-17,0 -28.5,-11.5T440,480q0,-17 11.5,-28.5T480,440q17,0 28.5,11.5T520,480q0,17 -11.5,28.5T480,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M226,788q-21.75,0 -37.87,-16.13Q172,755.75 172,734v-508q0,-21.75 16.13,-37.88Q204.25,172 226,172h205q-13,-27.5 2.95,-53.75 15.94,-26.25 46,-26.25Q511,92 527,118.25q16,26.25 2,53.75h205q21.75,0 37.88,16.12Q788,204.25 788,226v508q0,21.75 -16.12,37.87Q755.75,788 734,788L226,788ZM226,766h508q12,0 22,-10t10,-22v-508q0,-12 -10,-22t-22,-10L226,194q-12,0 -22,10t-10,22v508q0,12 10,22t22,10ZM306,642h226v-22L306,620v22ZM306,491h348v-22L306,469v22ZM306,340h348v-22L306,318v22ZM479.79,178q12.79,0 22.5,-9t9.71,-22.5q0,-13.5 -9.71,-22.5t-22.5,-9Q467,115 458,124t-9,22.5q0,13.5 9,22.5t21.79,9ZM194,766v-572,572Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M479.99,839q-49.99,0 -84.49,-34.51Q361,769.98 361,720q0,-47 31,-80.5t77,-38.5v-110L255,491v-136h-98v-218h218v218h-98v114h406v-114q-46,-5 -77,-38.29 -31,-33.3 -31,-80.71 0,-49.98 34.51,-84.49Q644.03,117 694.01,117q49.99,0 84.49,34.51Q813,186.02 813,236q0,47 -31,80.5T705,355v136L491,491v110q46,5 77,38.29 31,33.3 31,80.71 0,49.98 -34.51,84.49Q529.97,839 479.99,839ZM693.87,333q40.13,0 68.63,-28.37 28.5,-28.37 28.5,-68.5 0,-40.13 -28.37,-68.63 -28.37,-28.5 -68.5,-28.5 -40.13,0 -68.63,28.37 -28.5,28.37 -28.5,68.5 0,40.13 28.37,68.63 28.37,28.5 68.5,28.5ZM179,333h174v-174L179,159v174ZM479.87,817q40.13,0 68.63,-28.37 28.5,-28.37 28.5,-68.5 0,-40.13 -28.37,-68.63 -28.37,-28.5 -68.5,-28.5 -40.13,0 -68.63,28.37 -28.5,28.37 -28.5,68.5 0,40.13 28.37,68.63 28.37,28.5 68.5,28.5ZM266,246ZM694,236ZM480,720Z"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M278,566v22h-92q-21.75,0 -37.87,-16.13Q132,555.75 132,534v-348q0,-21.75 16.13,-37.88Q164.25,132 186,132h348q21.75,0 37.88,16.12Q588,164.25 588,186v92h-22v-92q0,-14 -9,-23t-23,-9L186,154q-14,0 -23,9t-9,23v348q0,14 9,23t23,9h92ZM426,828q-21.75,0 -37.87,-16.13Q372,795.75 372,774v-348q0,-21.75 16.13,-37.88Q404.25,372 426,372h348q21.75,0 37.88,16.12Q828,404.25 828,426v348q0,21.75 -16.12,37.87Q795.75,828 774,828L426,828ZM426,806h348q14,0 23,-9t9,-23v-348q0,-14 -9,-23t-23,-9L426,394q-14,0 -23,9t-9,23v348q0,14 9,23t23,9ZM600,600Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M480,680q14,0 23.5,-9.5T513,647q0,-14 -9.5,-23.5T480,614q-14,0 -23.5,9.5T447,647q0,14 9.5,23.5T480,680ZM453,527h60v-253h-60v253ZM481,931L346,800L160,800v-186L26,480l134,-134v-186h186l135,-134 133,134h186v186l134,134 -134,134v186L614,800L481,931Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="1024"
android:viewportWidth="1024"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:fillType="evenOdd"
android:pathData="M512,0C229.1,0 0,229.1 0,512C0,738.6 146.6,929.9 350.1,997.8C375.7,1002.2 385.3,986.9 385.3,973.4C385.3,961.3 384.6,921 384.6,878.1C256,901.8 222.7,846.7 212.5,817.9C206.7,803.2 181.8,757.8 160,745.6C142.1,736 116.5,712.3 159.4,711.7C199.7,711 228.5,748.8 238.1,764.2C284.2,841.6 357.8,819.8 387.2,806.4C391.7,773.1 405.1,750.7 419.8,737.9C305.9,725.1 186.9,681 186.9,485.1C186.9,429.4 206.7,383.4 239.4,347.5C234.2,334.7 216.3,282.2 244.5,211.8C244.5,211.8 287.4,198.4 385.3,264.3C426.2,252.8 469.8,247 513.3,247C556.8,247 600.3,252.8 641.3,264.3C739.2,197.8 782.1,211.8 782.1,211.8C810.2,282.2 792.3,334.7 787.2,347.5C819.8,383.4 839.7,428.8 839.7,485.1C839.7,681.6 720,725.1 606.1,737.9C624.6,753.9 640.6,784.6 640.6,832.6C640.6,901.1 640,956.2 640,973.4C640,986.9 649.6,1002.9 675.2,997.8C877.4,929.9 1024,737.9 1024,512C1024,229.1 794.9,0 512,0Z"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m612,668 l56,-56 -148,-148v-184h-80v216l172,172ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m612,668 l56,-56 -148,-148v-184h-80v216l172,172ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,480ZM480,800q133,0 226.5,-93.5T800,480q0,-133 -93.5,-226.5T480,160q-133,0 -226.5,93.5T160,480q0,133 93.5,226.5T480,800Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="M160,880q-33,0 -56.5,-23.5T80,800v-560h80v560h560v80L160,880ZM320,720q-33,0 -56.5,-23.5T240,640v-480q0,-33 23.5,-56.5T320,80h480q33,0 56.5,23.5T880,160v480q0,33 -23.5,56.5T800,720L320,720ZM520,440 L620,380 720,440v-280L520,160v280Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="M320,640h480v-480h-80v280l-100,-60 -100,60v-280L320,160v480ZM320,720q-33,0 -56.5,-23.5T240,640v-480q0,-33 23.5,-56.5T320,80h480q33,0 56.5,23.5T880,160v480q0,33 -23.5,56.5T800,720L320,720ZM160,880q-33,0 -56.5,-23.5T80,800v-560h80v560h560v80L160,880ZM520,160h200,-200ZM320,160h480,-480Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -1,45 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="256"
android:viewportWidth="256"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M111.07,140.52c0,-6.98 -5.69,-12.67 -12.67,-12.67c-6.98,0 -12.67,5.69 -12.67,12.67c0,6.98 5.66,12.6 12.67,12.67C105.38,153.19 111.07,147.5 111.07,140.52z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81" />
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M154.26,170.73c-7.7,5.5 -17,8.2 -26.43,7.7c-9.43,0.44 -18.7,-2.39 -26.31,-7.95c-1.29,-1.04 -3.08,-1.04 -4.34,0c-1.45,1.19 -1.67,3.36 -0.47,4.81c8.99,6.76 19.99,10.22 31.24,9.75c11.25,0.47 22.26,-2.99 31.24,-9.75l0,0.5c1.38,-1.35 1.41,-3.61 0.06,-5C157.9,169.41 155.64,169.38 154.26,170.73z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81" />
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M156.93,128.29c-6.98,0 -12.67,5.69 -12.67,12.67c0,6.98 5.69,12.67 12.67,12.67l-0.1,0.47c0.22,0 0.41,0 0.63,0c6.98,-0.28 12.41,-6.16 12.13,-13.14C169.6,133.99 163.91,128.29 156.93,128.29z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81" />
<path
android:fillColor="#e8eaed"
android:fillType="nonZero"
android:pathData="M127.86,1.41C58.02,1.41 1.41,58.02 1.41,127.86c0,69.84 56.61,126.45 126.45,126.45c69.84,0 126.45,-56.61 126.45,-126.45C254.31,58.02 197.69,1.41 127.86,1.41zM201.97,144.67c0.13,1.85 0.13,3.71 0,5.56c0,28.35 -33,51.36 -73.71,51.36s-73.71,-23.04 -73.71,-51.36c-0.13,-1.85 -0.13,-3.71 0,-5.56c-1.85,-0.82 -3.52,-1.98 -4.97,-3.33c-7.45,-6.98 -7.79,-18.67 -0.82,-26.09c6.98,-7.45 18.67,-7.79 26.09,-0.82c14.58,-9.87 31.71,-15.28 49.32,-15.56l9.33,-43.88c0,-0.03 0,-0.03 0,-0.06c0.47,-2.14 2.58,-3.46 4.68,-2.99l30.99,6.19c1.98,-3.46 5.56,-5.79 9.55,-6.19c6.95,-0.76 13.17,4.27 13.92,11.22s-4.27,13.17 -11.22,13.92c-6.95,0.76 -13.17,-4.27 -13.92,-11.22l-27.06,-5.69l-8.2,39.45c17.38,0.38 34.29,5.75 48.69,15.56c3.27,-3.11 7.57,-4.97 12.1,-5.12c10.21,-0.35 18.77,7.61 19.14,17.79C212.31,134.96 208.32,141.5 201.97,144.67z"
android:strokeColor="#00000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter"
android:strokeWidth="2.81"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,12 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="128dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="128dp">
<path
android:fillColor="#e8eaed"
android:pathData="M184.69,780v-30.77h81.69v-234L84.62,240h395.07L297.15,515.23v234h82.47L379.62,780L184.69,780ZM194.62,353.69h174.3l54.23,-82.92h-282l53.47,82.92ZM613.55,780q-42.4,0 -70.28,-27.79 -27.89,-27.8 -27.89,-70.2 0,-41.63 27.83,-69.51 27.82,-27.88 70.25,-27.88 16.84,0 33.27,6.07 16.42,6.08 33.27,18.77L680,240h155.38v61.31L710.77,301.31v380.61q0,42.44 -27.79,70.26Q655.18,780 613.55,780Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:height="39.867767dp"
android:viewportHeight="402"
android:viewportWidth="242"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m121,1c-9.32,10.58 -15,24.42 -15,39.62 0.55,33.37 4.64,66.7 5,100 0.74,31.1 -8.58,60.3 -19.92,88.8 -3.78,7.87 -7.86,15.57 -11.95,23.29l-12,-2.42c-10.83,-2.16 -17.87,-12.69 -15.7,-23.52 1.89,-9.47 10.23,-16.1 19.53,-16.1l4.38,0.47l-9.77,-81.6c-3.19,-36.3 -21.93,-68.2 -49.5,-88.9 -4.74,-3.56 -9.77,-6.82 -15,-9.69v289.7h66.88c5,27.1 19.94,50.8 40.94,67 4.99,3.44 9.18,7.98 12.19,13.28 3,-5.3 7.19,-9.84 12.19,-13.28 21,-16.24 35.94,-39.89 40.94,-67h66.88v-289.7c-5.23,2.87 -10.26,6.13 -15,9.69 -27.6,20.73 -46.3,52.6 -49.5,88.9l-9.77,81.6l4.38,-0.47c9.3,0.04 17.64,6.62 19.53,16.1 2.16,10.83 -4.88,21.35 -15.7,23.52l-12,2.42c-4.09,-7.71 -8.18,-15.41 -11.95,-23.29 -11.35,-28.48 -20.67,-57.68 -19.93,-88.8 0.36,-33.4 4.45,-66.7 5,-100 0,-15.2 -5.68,-29 -15,-39.62zM21,72.7c12.94,15.19 21.63,34.1 24.2,54.93l8,67.3c-10.25,5.1 -18,14.46 -20.94,25.79h-11.33v-148zM221,72.7v148h-11.33c-2.92,-11.32 -10.69,-20.68 -20.94,-25.79l8,-67.3c2.59,-20.81 11.28,-39.74 24.2,-54.94zM121,208c5.37,17.57 12.76,34.3 21.95,49.78 -8.74,2.65 -16.34,7.81 -21.95,14.76 -5.61,-6.95 -13.21,-12.1 -21.95,-14.77 9.19,-15.51 16.58,-32.2 21.95,-49.77zM21,240.74h11.33c3.57,13.81 14.35,24.71 28,28.52l9.61,2.19c-2.57,9.32 -3.98,19.1 -3.98,29.3h-45v-60zM209.7,240.74h11.33v60h-45c0,-10.14 -1.41,-19.97 -3.98,-29.3l9.61,-2.19c13.7,-3.81 24.47,-14.72 28,-28.52zM89.5,276.04c12.13,1.71 21.48,12.1 21.48,24.69h-25c0,-8.57 1.27,-16.84 3.52,-24.69zM152.47,276.04c2.25,7.85 3.52,16.1 3.52,24.69h-25c0,-12.61 9.35,-22.98 21.48,-24.69zM88.27,320.74h22.73v42.1c-11,-11.55 -19,-26 -22.73,-42.1zM131,320.74h22.73c-3.68,16.1 -11.7,30.57 -22.73,42.1v-42.1z"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="21.68dp"
android:viewportHeight="271"
android:viewportWidth="300"
android:width="24dp">
<path
android:fillColor="#e8eaed"
android:pathData="m236,0h46l-101,115 118,156h-92.6l-72.5,-94.8 -83,94.8h-46l107,-123 -113,-148h94.9l65.5,86.6zM219.9,244h25.5l-165,-218h-27.4z" />
</vector>

View file

@ -101,7 +101,9 @@
<string name="error_query">Помилка: %1$s</string> <string name="error_query">Помилка: %1$s</string>
<string name="error_file_empty">Цей файл пустий або пошкоджений.</string> <string name="error_file_empty">Цей файл пустий або пошкоджений.</string>
<string name="error_no_translator">Не знайдено жодного перекладача.</string> <string name="error_no_translator">Не знайдено жодного перекладача.</string>
<string name="error_no_dictionary">Не знайдено жодного словника.</string>
<string name="error_no_browser">Не знайдено жодного браузера.</string> <string name="error_no_browser">Не знайдено жодного браузера.</string>
<string name="error_no_share_app">Не знайдено застосунка щоб поділитися.</string>
<string name="error_no_text"> <string name="error_no_text">
Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її. Вміст книги пустий. Будь ласка, перевірте вміст книги та оновіть її.
</string> </string>
@ -160,6 +162,8 @@
<string name="download">Завантажити</string> <string name="download">Завантажити</string>
<string name="done">Готово</string> <string name="done">Готово</string>
<string name="enable">Увімкнути</string> <string name="enable">Увімкнути</string>
<string name="web_search">Веб-пошук</string>
<string name="share">Поділитися</string>
<!-- Items --> <!-- Items -->
<string name="files">файли</string> <string name="files">файли</string>

View file

@ -118,7 +118,9 @@
<string name="error_query">Error: %1$s</string> <string name="error_query">Error: %1$s</string>
<string name="error_file_empty">This file is empty or corrupted.</string> <string name="error_file_empty">This file is empty or corrupted.</string>
<string name="error_no_translator">No translator app found.</string> <string name="error_no_translator">No translator app found.</string>
<string name="error_no_dictionary">No dictionary app found.</string>
<string name="error_no_browser">No browser app found.</string> <string name="error_no_browser">No browser app found.</string>
<string name="error_no_share_app">No app to share found.</string>
<string name="error_no_text"> <string name="error_no_text">
The book content is empty. Please, make sure that the book content is not empty and update it. The book content is empty. Please, make sure that the book content is not empty and update it.
</string> </string>
@ -176,6 +178,8 @@
<string name="download">Download</string> <string name="download">Download</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="enable">Enable</string> <string name="enable">Enable</string>
<string name="web_search">Web search</string>
<string name="share">Share</string>
<!-- Items --> <!-- Items -->
<string name="files">files</string> <string name="files">files</string>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

View file

@ -0,0 +1,8 @@
* Added loading indicator in AboutScreen's check for updates
* Reduced APK size (30Mb → 12Mb)
* Improved performance
* Improved text parsers
* Fixed various crashes
* Various small changes
* Removed no longer valid Credits
* More small changes

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

After

Width:  |  Height:  |  Size: 983 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

After

Width:  |  Height:  |  Size: 947 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 985 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 KiB

After

Width:  |  Height:  |  Size: 947 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB