🚀 Crash screen
* Shows app crash log * Copy or report (share) * Logs are saved in app's internal storage (/crash) Resolves: #188
This commit is contained in:
parent
13da03f2aa
commit
ba3f5f3b7a
14 changed files with 468 additions and 4 deletions
|
|
@ -35,6 +35,7 @@
|
||||||
tools:ignore="DataExtractionRules"
|
tools:ignore="DataExtractionRules"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
|
<!-- Main activity -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
|
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
|
||||||
|
|
@ -47,6 +48,14 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Crash activity -->
|
||||||
|
<activity
|
||||||
|
android:name=".ui.crash.CrashActivity"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@style/BookStory"
|
||||||
|
android:windowSoftInputMode="adjustResize"></activity>
|
||||||
|
|
||||||
<!-- Save app locales -->
|
<!-- Save app locales -->
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ package ua.acclorite.book_story
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import ua.acclorite.book_story.core.crash.CrashHandler
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class Application : Application()
|
class Application : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(CrashHandler(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.core.crash
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Process
|
||||||
|
import android.util.Log
|
||||||
|
import ua.acclorite.book_story.ui.crash.CrashActivity
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
private const val CRASH_TAG = "CRASH, APP"
|
||||||
|
|
||||||
|
class CrashHandler(
|
||||||
|
private val context: Context
|
||||||
|
) : Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
|
override fun uncaughtException(thread: Thread, throwable: Throwable) {
|
||||||
|
val crashLog = throwable.stackTraceToString()
|
||||||
|
|
||||||
|
Log.e(CRASH_TAG, crashLog)
|
||||||
|
if (!CrashUtils.saveCrashLog(context, crashLog)) return
|
||||||
|
|
||||||
|
val intent = Intent(context, CrashActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
|
||||||
|
Process.killProcess(Process.myPid())
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.core.crash
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.BufferedWriter
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
object CrashUtils {
|
||||||
|
|
||||||
|
fun saveCrashLog(context: Context, crashLog: String): Boolean {
|
||||||
|
val crashDir = File(context.filesDir, "crash")
|
||||||
|
if (!crashDir.exists()) crashDir.mkdirs()
|
||||||
|
|
||||||
|
val currentTime = LocalDateTime.now()
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd_HH.mm.ss")
|
||||||
|
val formattedDateTime = currentTime.format(formatter)
|
||||||
|
|
||||||
|
val fileName = "crashLog_$formattedDateTime.txt"
|
||||||
|
val crashFile = File(crashDir, fileName)
|
||||||
|
val tempFile = File(crashDir, "$fileName.tmp")
|
||||||
|
|
||||||
|
try {
|
||||||
|
BufferedWriter(FileWriter(tempFile)).use { writer ->
|
||||||
|
writer.write(crashLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tempFile.renameTo(crashFile)) {
|
||||||
|
throw IllegalStateException("Failed to rename .tmp file.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
tempFile.delete()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCrashLog(context: Context): String? {
|
||||||
|
val crashDir = File(context.filesDir, "crash")
|
||||||
|
if (!crashDir.exists()) return null
|
||||||
|
|
||||||
|
val crashLogs = crashDir.listFiles() ?: return null
|
||||||
|
val crashLog = crashLogs.maxByOrNull { it.lastModified() } ?: return null
|
||||||
|
return crashLog.bufferedReader().use { reader ->
|
||||||
|
reader.readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,6 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
private const val EPUB_TAG = "EPUB Parser"
|
private const val EPUB_TAG = "EPUB Parser"
|
||||||
private typealias Source = String
|
private typealias Source = String
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.presentation.crash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CrashBottomBar(
|
||||||
|
copy: () -> Unit,
|
||||||
|
report: () -> Unit
|
||||||
|
) {
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val copyWidth = remember(windowInfo.containerSize) {
|
||||||
|
with(density) {
|
||||||
|
(windowInfo.containerSize.width.toDp() - 36.dp) * (1f / 2.5f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(top = 18.dp, bottom = 8.dp)
|
||||||
|
.padding(horizontal = 18.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(18.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.widthIn(
|
||||||
|
min = copyWidth
|
||||||
|
),
|
||||||
|
onClick = { copy() }
|
||||||
|
) {
|
||||||
|
StyledText(
|
||||||
|
text = stringResource(id = R.string.copy),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { report() }
|
||||||
|
) {
|
||||||
|
StyledText(
|
||||||
|
text = stringResource(id = R.string.report),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.presentation.crash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CrashContent(
|
||||||
|
crashLog: String?,
|
||||||
|
copy: () -> Unit,
|
||||||
|
report: () -> Unit
|
||||||
|
) {
|
||||||
|
CrashScaffold(
|
||||||
|
copy = copy,
|
||||||
|
report = report
|
||||||
|
) {
|
||||||
|
CrashLayout {
|
||||||
|
if (!crashLog.isNullOrBlank()) {
|
||||||
|
item {
|
||||||
|
SelectionContainer {
|
||||||
|
StyledText(
|
||||||
|
text = crashLog,
|
||||||
|
modifier = Modifier.horizontalScroll(rememberScrollState()),
|
||||||
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.presentation.crash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CrashLayout(
|
||||||
|
content: LazyListScope.() -> Unit
|
||||||
|
) {
|
||||||
|
LazyColumnWithScrollbar(
|
||||||
|
Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(18.dp)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.presentation.crash
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BugReport
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CrashScaffold(
|
||||||
|
copy: () -> Unit,
|
||||||
|
report: () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
bottomBar = {
|
||||||
|
CrashBottomBar(
|
||||||
|
copy = copy,
|
||||||
|
report = report
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(it)
|
||||||
|
.padding(horizontal = 18.dp)
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.BugReport,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
StyledText(
|
||||||
|
text = stringResource(id = R.string.crash_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
StyledText(
|
||||||
|
text = stringResource(id = R.string.crash_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(18.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.ui.crash
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import ua.acclorite.book_story.domain.ui.ThemeContrast
|
||||||
|
import ua.acclorite.book_story.ui.theme.BookStoryTheme
|
||||||
|
import ua.acclorite.book_story.ui.theme.Theme
|
||||||
|
|
||||||
|
class CrashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
// Default super
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Edge to edge
|
||||||
|
enableEdgeToEdge()
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
BookStoryTheme(
|
||||||
|
theme = Theme.DYNAMIC,
|
||||||
|
isDark = isSystemInDarkTheme(),
|
||||||
|
isPureDark = false,
|
||||||
|
themeContrast = ThemeContrast.STANDARD
|
||||||
|
) {
|
||||||
|
CrashScreen.Content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2025 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ua.acclorite.book_story.ui.crash
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.core.crash.CrashUtils
|
||||||
|
import ua.acclorite.book_story.domain.navigator.Screen
|
||||||
|
import ua.acclorite.book_story.presentation.core.util.LocalActivity
|
||||||
|
import ua.acclorite.book_story.presentation.core.util.launchActivity
|
||||||
|
import ua.acclorite.book_story.presentation.core.util.showToast
|
||||||
|
import ua.acclorite.book_story.presentation.crash.CrashContent
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object CrashScreen : Screen, Parcelable {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val activity = LocalActivity.current
|
||||||
|
val clipboard = LocalClipboard.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val crashLog = remember {
|
||||||
|
CrashUtils.getCrashLog(activity)?.trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
CrashContent(
|
||||||
|
crashLog = crashLog,
|
||||||
|
copy = {
|
||||||
|
if (crashLog.isNullOrBlank()) return@CrashContent
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
clipboard.setClipEntry(
|
||||||
|
ClipEntry(ClipData.newPlainText("Crash", crashLog))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||||
|
activity.getString(R.string.copied)
|
||||||
|
.showToast(context = activity, longToast = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
report = {
|
||||||
|
Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_SUBJECT,
|
||||||
|
activity.getString(R.string.app_name)
|
||||||
|
)
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
crashLog
|
||||||
|
)
|
||||||
|
}.launchActivity(
|
||||||
|
activity = activity,
|
||||||
|
createChooser = true,
|
||||||
|
success = {
|
||||||
|
return@CrashContent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
activity.getString(R.string.error_no_share_app)
|
||||||
|
.showToast(context = activity, longToast = false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,8 +37,6 @@ import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.component1
|
|
||||||
import kotlin.collections.component2
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HistoryModel @Inject constructor(
|
class HistoryModel @Inject constructor(
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
<string name="never">Ніколи</string>
|
<string name="never">Ніколи</string>
|
||||||
<string name="undo">Скасувати</string>
|
<string name="undo">Скасувати</string>
|
||||||
<string name="copy">Копіювати</string>
|
<string name="copy">Копіювати</string>
|
||||||
|
<string name="report">Повідомити</string>
|
||||||
<string name="translate">Перекласти</string>
|
<string name="translate">Перекласти</string>
|
||||||
<string name="dictionary">Словник</string>
|
<string name="dictionary">Словник</string>
|
||||||
<string name="next">Далі</string>
|
<string name="next">Далі</string>
|
||||||
|
|
@ -431,6 +432,10 @@
|
||||||
<string name="today">Сьогодні</string>
|
<string name="today">Сьогодні</string>
|
||||||
<string name="yesterday">Вчора</string>
|
<string name="yesterday">Вчора</string>
|
||||||
|
|
||||||
|
<!-- Crash -->
|
||||||
|
<string name="crash_title">Ой! Застосунок крашнувся.</string>
|
||||||
|
<string name="crash_description">Будь ласка, повідомте про цей збій у найзручніший для вас спосіб.</string>
|
||||||
|
|
||||||
<!-- Content description -->
|
<!-- Content description -->
|
||||||
<string name="continue_reading_content_desc">Продовжити читання</string>
|
<string name="continue_reading_content_desc">Продовжити читання</string>
|
||||||
<string name="go_back_content_desc">Назад</string>
|
<string name="go_back_content_desc">Назад</string>
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@
|
||||||
<string name="never">Never</string>
|
<string name="never">Never</string>
|
||||||
<string name="undo">Undo</string>
|
<string name="undo">Undo</string>
|
||||||
<string name="copy">Copy</string>
|
<string name="copy">Copy</string>
|
||||||
|
<string name="report">Report</string>
|
||||||
<string name="translate">Translate</string>
|
<string name="translate">Translate</string>
|
||||||
<string name="dictionary">Dictionary</string>
|
<string name="dictionary">Dictionary</string>
|
||||||
<string name="next">Next</string>
|
<string name="next">Next</string>
|
||||||
|
|
@ -566,6 +567,10 @@
|
||||||
<string name="today">Today</string>
|
<string name="today">Today</string>
|
||||||
<string name="yesterday">Yesterday</string>
|
<string name="yesterday">Yesterday</string>
|
||||||
|
|
||||||
|
<!-- Crash -->
|
||||||
|
<string name="crash_title">Oops! The app crashed.</string>
|
||||||
|
<string name="crash_description">Please report this crash message in the most convenient way for you.</string>
|
||||||
|
|
||||||
<!-- Content description -->
|
<!-- Content description -->
|
||||||
<string name="continue_reading_content_desc">Continue reading</string>
|
<string name="continue_reading_content_desc">Continue reading</string>
|
||||||
<string name="go_back_content_desc">Go back</string>
|
<string name="go_back_content_desc">Go back</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue