Merge branch 'master' of https://github.com/tachiyomiorg/tachiyomi into sync-part-final

This commit is contained in:
KaiserBh
2024-01-07 10:00:28 +11:00
76 changed files with 825 additions and 536 deletions

View File

@@ -22,8 +22,8 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 114
versionName = "0.14.7"
versionCode = 115
versionName = "0.15.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")

View File

@@ -41,6 +41,7 @@ internal class GuidesStep(
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)

View File

@@ -5,6 +5,7 @@ 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.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -14,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.more.settings.screen.SettingsDataScreen
import eu.kanade.tachiyomi.util.system.toast
@@ -38,6 +40,8 @@ internal class StorageStep : OnboardingStep {
@Composable
override fun Content() {
val context = LocalContext.current
val handler = LocalUriHandler.current
val pickStorageLocation = SettingsDataScreen.storageLocationPicker(storagePref)
Column(
@@ -64,6 +68,19 @@ internal class StorageStep : OnboardingStep {
) {
Text(stringResource(MR.strings.onboarding_storage_action_select))
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
Text(stringResource(MR.strings.onboarding_storage_help_info, stringResource(MR.strings.app_name)))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { handler.openUri("https://tachiyomi.org/docs/faq/storage") },
) {
Text(stringResource(MR.strings.onboarding_storage_help_action))
}
}
LaunchedEffect(Unit) {

View File

@@ -1,34 +1,26 @@
package eu.kanade.presentation.more.settings.screen
import android.app.Activity
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
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.core.app.ActivityCompat
import androidx.core.os.LocaleListCompat
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.appearance.AppLanguageScreen
import eu.kanade.presentation.more.settings.widget.AppThemeModePreferenceWidget
import eu.kanade.presentation.more.settings.widget.AppThemePreferenceWidget
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableMap
import org.xmlpull.v1.XmlPullParser
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@@ -107,11 +99,8 @@ object SettingsAppearanceScreen : SearchableSettings {
uiPreferences: UiPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val langs = remember { getLangs(context) }
var currentLanguage by remember {
mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "")
}
val now = remember { Instant.now().toEpochMilli() }
val dateFormat by uiPreferences.dateFormat().collectAsState()
@@ -119,26 +108,12 @@ object SettingsAppearanceScreen : SearchableSettings {
UiPreferences.dateFormat(dateFormat).format(now)
}
LaunchedEffect(currentLanguage) {
val locale = if (currentLanguage.isEmpty()) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(currentLanguage)
}
AppCompatDelegate.setApplicationLocales(locale)
}
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_display),
preferenceItems = persistentListOf(
Preference.PreferenceItem.BasicListPreference(
value = currentLanguage,
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_app_language),
entries = langs,
onValueChanged = { newValue ->
currentLanguage = newValue
true
},
onClick = { navigator.push(AppLanguageScreen()) },
),
Preference.PreferenceItem.ListPreference(
pref = uiPreferences.tabletUiMode(),
@@ -173,30 +148,6 @@ object SettingsAppearanceScreen : SearchableSettings {
),
)
}
private fun getLangs(context: Context): ImmutableMap<String, String> {
val langs = mutableListOf<Pair<String, String>>()
val parser = context.resources.getXml(R.xml.locales_config)
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
for (i in 0..<parser.attributeCount) {
if (parser.getAttributeName(i) == "name") {
val langTag = parser.getAttributeValue(i)
val displayName = LocaleHelper.getDisplayName(langTag)
if (displayName.isNotEmpty()) {
langs.add(Pair(langTag, displayName))
}
}
}
}
eventType = parser.next()
}
langs.sortBy { it.second }
langs.add(0, Pair("", context.stringResource(MR.strings.label_default)))
return langs.toMap().toImmutableMap()
}
}
private val DateFormats = listOf(

View File

@@ -0,0 +1,128 @@
package eu.kanade.presentation.more.settings.screen.appearance
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.platform.LocalContext
import androidx.core.os.LocaleListCompat
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.xmlpull.v1.XmlPullParser
import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import java.util.Locale
class AppLanguageScreen : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val langs = remember { getLangs(context) }
var currentLanguage by remember {
mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "")
}
LaunchedEffect(currentLanguage) {
val locale = if (currentLanguage.isEmpty()) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(currentLanguage)
}
AppCompatDelegate.setApplicationLocales(locale)
}
Scaffold(
topBar = { scrollBehavior ->
AppBar(
title = stringResource(MR.strings.pref_app_language),
navigateUp = navigator::pop,
scrollBehavior = scrollBehavior,
)
},
) { contentPadding ->
LazyColumn(
modifier = Modifier.padding(contentPadding),
) {
items(langs) {
ListItem(
modifier = Modifier.clickable {
currentLanguage = it.langTag
},
headlineContent = { Text(it.displayName) },
supportingContent = {
it.localizedDisplayName?.let {
Text(it)
}
},
trailingContent = {
if (currentLanguage == it.langTag) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
},
)
}
}
}
}
private fun getLangs(context: Context): ImmutableList<Language> {
val langs = mutableListOf<Language>()
val parser = context.resources.getXml(R.xml.locales_config)
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
for (i in 0..<parser.attributeCount) {
if (parser.getAttributeName(i) == "name") {
val langTag = parser.getAttributeValue(i)
val displayName = LocaleHelper.getDisplayName(langTag)
if (displayName.isNotEmpty()) {
langs.add(Language(langTag, displayName, Locale.forLanguageTag(langTag).displayName))
}
}
}
}
eventType = parser.next()
}
langs.sortBy { it.displayName }
langs.add(0, Language("", context.stringResource(MR.strings.label_default), null))
return langs.toImmutableList()
}
private data class Language(
val langTag: String,
val displayName: String,
val localizedDisplayName: String?,
)
}

View File

@@ -10,6 +10,7 @@ import java.io.File
import java.io.InputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.util.concurrent.Executors
/**
* Loader used to load a chapter from a .rar or .cbr file.
@@ -20,13 +21,18 @@ internal class RarPageLoader(file: File) : PageLoader() {
override var isLocal: Boolean = true
/**
* Pool for copying compressed files to an input stream.
*/
private val pool = Executors.newFixedThreadPool(1)
override suspend fun getPages(): List<ReaderPage> {
return rar.fileHeaders.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header ->
ReaderPage(i).apply {
stream = { getStream(rar, header) }
stream = { getStream(header) }
status = Page.State.READY
}
}
@@ -40,15 +46,16 @@ internal class RarPageLoader(file: File) : PageLoader() {
override fun recycle() {
super.recycle()
rar.close()
pool.shutdown()
}
/**
* Returns an input stream for the given [header].
*/
private fun getStream(rar: Archive, header: FileHeader): InputStream {
private fun getStream(header: FileHeader): InputStream {
val pipeIn = PipedInputStream()
val pipeOut = PipedOutputStream(pipeIn)
synchronized(this) {
pool.execute {
try {
pipeOut.use {
rar.extractFile(header, it)

View File

@@ -235,7 +235,7 @@ class PagerPageHolder(
*/
private fun setError() {
progressIndicator.hide()
showErrorLayout(withOpenInWebView = false)
showErrorLayout()
}
override fun onImageLoaded() {
@@ -248,8 +248,7 @@ class PagerPageHolder(
*/
override fun onImageLoadError() {
super.onImageLoadError()
progressIndicator.hide()
showErrorLayout(withOpenInWebView = true)
setError()
}
/**
@@ -260,23 +259,27 @@ class PagerPageHolder(
viewer.activity.hideMenu()
}
private fun showErrorLayout(withOpenInWebView: Boolean): ReaderErrorBinding {
private fun showErrorLayout(): ReaderErrorBinding {
if (errorLayout == null) {
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), this, true)
errorLayout?.actionRetry?.viewer = viewer
errorLayout?.actionRetry?.setOnClickListener {
page.chapter.pageLoader?.retryPage(page)
}
val imageUrl = page.imageUrl
if (imageUrl.orEmpty().startsWith("http", true)) {
}
val imageUrl = page.imageUrl
errorLayout?.actionOpenInWebView?.isVisible = imageUrl != null
if (imageUrl != null) {
if (imageUrl.startsWith("http", true)) {
errorLayout?.actionOpenInWebView?.viewer = viewer
errorLayout?.actionOpenInWebView?.setOnClickListener {
val intent = WebViewActivity.newIntent(context, imageUrl!!)
val intent = WebViewActivity.newIntent(context, imageUrl)
context.startActivity(intent)
}
}
}
errorLayout?.actionOpenInWebView?.isVisible = withOpenInWebView
errorLayout?.root?.isVisible = true
return errorLayout!!
}

View File

@@ -80,7 +80,7 @@ class WebtoonPageHolder(
refreshLayoutParams()
frame.onImageLoaded = { onImageDecoded() }
frame.onImageLoadError = { onImageDecodeError() }
frame.onImageLoadError = { setError() }
frame.onScaleChanged = { viewer.activity.hideMenu() }
}
@@ -240,7 +240,7 @@ class WebtoonPageHolder(
*/
private fun setError() {
progressContainer.isVisible = false
initErrorLayout(withOpenInWebView = false)
initErrorLayout()
}
/**
@@ -251,14 +251,6 @@ class WebtoonPageHolder(
removeErrorLayout()
}
/**
* Called when the image fails to decode.
*/
private fun onImageDecodeError() {
progressContainer.isVisible = false
initErrorLayout(withOpenInWebView = true)
}
/**
* Creates a new progress bar.
*/
@@ -278,22 +270,26 @@ class WebtoonPageHolder(
/**
* Initializes a button to retry pages.
*/
private fun initErrorLayout(withOpenInWebView: Boolean): ReaderErrorBinding {
private fun initErrorLayout(): ReaderErrorBinding {
if (errorLayout == null) {
errorLayout = ReaderErrorBinding.inflate(LayoutInflater.from(context), frame, true)
errorLayout?.root?.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, (parentHeight * 0.8).toInt())
errorLayout?.actionRetry?.setOnClickListener {
page?.let { it.chapter.pageLoader?.retryPage(it) }
}
val imageUrl = page?.imageUrl
if (imageUrl.orEmpty().startsWith("http", true)) {
}
val imageUrl = page?.imageUrl
errorLayout?.actionOpenInWebView?.isVisible = imageUrl != null
if (imageUrl != null) {
if (imageUrl.startsWith("http", true)) {
errorLayout?.actionOpenInWebView?.setOnClickListener {
val intent = WebViewActivity.newIntent(context, imageUrl!!)
val intent = WebViewActivity.newIntent(context, imageUrl)
context.startActivity(intent)
}
}
}
errorLayout?.actionOpenInWebView?.isVisible = withOpenInWebView
return errorLayout!!
}