diff --git a/lib-multisrc/machinetranslations/assets/i18n/messages_en.properties b/lib-multisrc/machinetranslations/assets/i18n/messages_en.properties index 135a66d80..d02b9d960 100644 --- a/lib-multisrc/machinetranslations/assets/i18n/messages_en.properties +++ b/lib-multisrc/machinetranslations/assets/i18n/messages_en.properties @@ -2,3 +2,5 @@ font_size_title=Font size font_size_summary=Font changes will not be applied to downloaded or cached chapters. The font size will be adjusted according to the size of the dialog box. font_size_message=Font size changed to %s default_font_size=Default +disable_website_setting_title=Disable source settings +disable_website_setting_summary=The site's fonts and colors will be disabled diff --git a/lib-multisrc/machinetranslations/assets/i18n/messages_pt_br.properties b/lib-multisrc/machinetranslations/assets/i18n/messages_pt_br.properties index 8ab7a029d..7ac1fe180 100644 --- a/lib-multisrc/machinetranslations/assets/i18n/messages_pt_br.properties +++ b/lib-multisrc/machinetranslations/assets/i18n/messages_pt_br.properties @@ -2,3 +2,5 @@ font_size_title=Tamanho da fonte font_size_summary=As alterações de fonte não serão aplicadas aos capítulos baixados ou armazenados em cache. O tamanho da fonte será ajustado de acordo com o tamanho da caixa de diálogo. font_size_message=Tamanho da fonte foi alterada para %s default_font_size=Padrão +disable_website_setting_title=Desativar configurações do site +disable_website_setting_summary=As fontes e cores do site serão desativadas diff --git a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslations.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslations.kt index eef9e39bd..506ed4a04 100644 --- a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslations.kt +++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslations.kt @@ -6,7 +6,9 @@ import android.os.Build import android.widget.Toast import androidx.annotation.RequiresApi import androidx.preference.ListPreference +import androidx.preference.Preference import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.lib.i18n.Intl import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor import eu.kanade.tachiyomi.network.GET @@ -22,6 +24,7 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import org.jsoup.nodes.Document @@ -33,12 +36,13 @@ import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale +import java.util.concurrent.TimeUnit @RequiresApi(Build.VERSION_CODES.O) abstract class MachineTranslations( override val name: String, override val baseUrl: String, - val language: Language, + private val language: Language, ) : ParsedHttpSource(), ConfigurableSource { override val supportsLatest = true @@ -47,14 +51,31 @@ abstract class MachineTranslations( override val lang = language.lang - private val preferences: SharedPreferences by lazy { + protected val preferences: SharedPreferences by lazy { Injekt.get().getSharedPreferences("source_$id", 0x0000) } + /** + * A flag that tracks whether the settings have been changed. It is used to indicate if + * any configuration change has occurred. Once the value is accessed, it resets to `false`. + * This is useful for tracking whether a preference has been modified, and ensures that + * the change status is cleared after it has been accessed, to prevent multiple triggers. + */ + private var isSettingsChanged: Boolean = false + get() { + val current = field + field = false + return current + } + protected var fontSize: Int get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt() set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply() + protected var disableSourceSettings: Boolean + get() = preferences.getBoolean(DISABLE_SOURCE_SETTINGS_PREF, language.disableSourceSettings) + set(value) = preferences.edit().putBoolean(DISABLE_SOURCE_SETTINGS_PREF, value).apply() + private val intl = Intl( language = language.lang, baseLanguage = "en", @@ -62,10 +83,33 @@ abstract class MachineTranslations( classLoader = this::class.java.classLoader!!, ) - override val client: OkHttpClient by lazy { - network.cloudflareClient.newBuilder() - .addInterceptor(ComposedImageInterceptor(baseUrl, language, fontSize)) - .build() + private val settings get() = language.apply { + fontSize = this@MachineTranslations.fontSize + } + + open val useDefaultComposedImageInterceptor: Boolean = true + + override val client: OkHttpClient get() = clientInstance!! + + /** + * This ensures that the `OkHttpClient` instance is only created when required, and it is rebuilt + * when there are configuration changes to ensure that the client uses the most up-to-date settings. + */ + private var clientInstance: OkHttpClient? = null + get() { + if (field == null || isSettingsChanged) { + field = clientBuilder().build() + } + return field + } + + protected open fun clientBuilder() = network.cloudflareClient.newBuilder() + .connectTimeout(1, TimeUnit.MINUTES) + .readTimeout(2, TimeUnit.MINUTES) + .addInterceptorIf(useDefaultComposedImageInterceptor, ComposedImageInterceptor(baseUrl, settings)) + + private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder { + return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor) } // ============================== Popular =============================== @@ -249,7 +293,7 @@ abstract class MachineTranslations( entryValues = sizes summary = intl["font_size_summary"] - setOnPreferenceChangeListener { _, newValue -> + setOnPreferenceChange { _, newValue -> val selected = newValue as String val index = this.findIndexOfValue(selected) val entry = entries[index] as String @@ -262,16 +306,43 @@ abstract class MachineTranslations( Toast.LENGTH_LONG, ).show() - false + true // It's necessary to update the user interface } }.also(screen::addPreference) + + if (language.disableSourceSettings.not()) { + SwitchPreferenceCompat(screen.context).apply { + key = DISABLE_SOURCE_SETTINGS_PREF + title = "⚠ ${intl["disable_website_setting_title"]}" + summary = intl["disable_website_setting_summary"] + setDefaultValue(false) + setOnPreferenceChange { _, newValue -> + disableSourceSettings = newValue as Boolean + true + } + }.also(screen::addPreference) + } + } + + /** + * Sets an `OnPreferenceChangeListener` for the preference, and before triggering the original listener, + * marks that the configuration has changed by setting `isSettingsChanged` to `true`. + * This behavior is useful for applying runtime configurations in the HTTP client, + * ensuring that the preference change is registered before invoking the original listener. + */ + protected fun Preference.setOnPreferenceChange(onPreferenceChangeListener: Preference.OnPreferenceChangeListener) { + setOnPreferenceChangeListener { preference, newValue -> + isSettingsChanged = true + onPreferenceChangeListener.onPreferenceChange(preference, newValue) + } } companion object { val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE) const val PREFIX_SEARCH = "id:" - const val FONT_SIZE_PREF = "fontSizePref" - const val DEFAULT_FONT_SIZE = "24" + private const val FONT_SIZE_PREF = "fontSizePref" + private const val DISABLE_SOURCE_SETTINGS_PREF = "disableSourceSettingsPref" + private const val DEFAULT_FONT_SIZE = "24" private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US) } diff --git a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFactoryUtils.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFactoryUtils.kt index 96c03fa74..7b87c533c 100644 --- a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFactoryUtils.kt +++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/MachineTranslationsFactoryUtils.kt @@ -2,4 +2,18 @@ package eu.kanade.tachiyomi.multisrc.machinetranslations class MachineTranslationsFactoryUtils -data class Language(val lang: String, val target: String = lang, val origin: String = "en") +interface Language { + val lang: String + val target: String + val origin: String + var fontSize: Int + var disableSourceSettings: Boolean +} + +data class LanguageImpl( + override val lang: String, + override val target: String = lang, + override val origin: String = "en", + override var fontSize: Int = 24, + override var disableSourceSettings: Boolean = false, +) : Language diff --git a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/interceptors/ComposedImageInterceptor.kt b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/interceptors/ComposedImageInterceptor.kt index 677416fed..ebbbc426a 100644 --- a/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/interceptors/ComposedImageInterceptor.kt +++ b/lib-multisrc/machinetranslations/src/eu/kanade/tachiyomi/multisrc/machinetranslations/interceptors/ComposedImageInterceptor.kt @@ -34,8 +34,7 @@ import kotlin.math.sqrt @RequiresApi(Build.VERSION_CODES.O) class ComposedImageInterceptor( baseUrl: String, - val language: Language, - val fontSize: Int = 24, + var language: Language, ) : Interceptor { private val json: Json by injectLazy() @@ -63,7 +62,9 @@ class ComposedImageInterceptor( // Load the fonts before opening the connection to load the image, // so there aren't two open connections inside the interceptor. - loadAllFont(chain) + if (language.disableSourceSettings.not()) { + loadAllFont(chain) + } val response = chain.proceed(imageRequest) @@ -104,7 +105,7 @@ class ComposedImageInterceptor( } private fun createTextPaint(font: Typeface?): TextPaint { - val defaultTextSize = fontSize.pt + val defaultTextSize = language.fontSize.pt return TextPaint().apply { color = Color.BLACK textSize = defaultTextSize @@ -116,6 +117,10 @@ class ComposedImageInterceptor( } private fun selectFontFamily(type: String): Typeface? { + if (language.disableSourceSettings) { + return null + } + if (type in fontFamily) { return fontFamily[type]?.second } @@ -218,7 +223,7 @@ class ComposedImageInterceptor( } // Use source setup - if (dialog.isNewApi) { + if (dialog.isNewApi && language.disableSourceSettings.not()) { textPaint.color = dialog.foregroundColor textPaint.bgColor = dialog.backgroundColor textPaint.style = if (dialog.isBold) Paint.Style.FILL_AND_STROKE else Paint.Style.FILL diff --git a/src/all/snowmtl/build.gradle b/src/all/snowmtl/build.gradle index 8b9aa33bf..1daee5940 100644 --- a/src/all/snowmtl/build.gradle +++ b/src/all/snowmtl/build.gradle @@ -3,7 +3,7 @@ ext { extClass = '.SnowmtlFactory' themePkg = 'machinetranslations' baseUrl = 'https://snowmtl.ru' - overrideVersionCode = 7 + overrideVersionCode = 8 isNsfw = true } diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt index 228e24ccc..96ef6264b 100644 --- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt +++ b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/Snowmtl.kt @@ -2,19 +2,19 @@ package eu.kanade.tachiyomi.extension.all.snowmtl import android.os.Build import androidx.annotation.RequiresApi +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat import eu.kanade.tachiyomi.extension.all.snowmtl.interceptors.TranslationInterceptor import eu.kanade.tachiyomi.extension.all.snowmtl.translator.BingTranslator import eu.kanade.tachiyomi.extension.all.snowmtl.translator.TranslatorEngine -import eu.kanade.tachiyomi.multisrc.machinetranslations.Language import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor import eu.kanade.tachiyomi.network.interceptor.rateLimit -import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit @RequiresApi(Build.VERSION_CODES.O) class Snowmtl( - language: Language, + private val language: LanguageSetting, ) : MachineTranslations( name = "Snow Machine Translations", baseUrl = "https://snowmtl.ru", @@ -22,18 +22,59 @@ class Snowmtl( ) { override val lang = language.lang + private var disableTranslationOptimization: Boolean + get() = preferences.getBoolean(DISABLE_TRANSLATION_OPTIM_PREF, language.disableTranslationOptimization) + set(value) = preferences.edit().putBoolean(DISABLE_TRANSLATION_OPTIM_PREF, value).apply() + + private val settings: LanguageSetting get() = language.copy( + fontSize = this@Snowmtl.fontSize, + disableTranslationOptimization = this@Snowmtl.disableTranslationOptimization, + disableSourceSettings = this@Snowmtl.disableSourceSettings, + ) + private val clientUtils = network.cloudflareClient.newBuilder() - .rateLimit(1, 2, TimeUnit.SECONDS) + .rateLimit(3, 2, TimeUnit.SECONDS) .build() private val translator: TranslatorEngine = BingTranslator(clientUtils, headers) - override val client: OkHttpClient by lazy { - network.cloudflareClient.newBuilder() - .connectTimeout(1, TimeUnit.MINUTES) - .readTimeout(2, TimeUnit.MINUTES) - .addInterceptor(TranslationInterceptor(language, translator)) - .addInterceptor(ComposedImageInterceptor(baseUrl, language, fontSize)) - .build() + // Keeps object state + private val composeInterceptor = ComposedImageInterceptor(baseUrl, settings) + private val translatorInterceptor = TranslationInterceptor(settings, translator) + + override val useDefaultComposedImageInterceptor = false + + override fun clientBuilder() = super.clientBuilder() + .rateLimit(3) + .addInterceptor(translatorInterceptor.apply { language = this@Snowmtl.settings }) + .addInterceptor(composeInterceptor.apply { language = this@Snowmtl.settings }) + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + super.setupPreferenceScreen(screen) + + if (language.target == language.origin) { + return + } + + if (language.disableTranslationOptimization.not()) { + SwitchPreferenceCompat(screen.context).apply { + key = DISABLE_TRANSLATION_OPTIM_PREF + title = "⚠ Disable translation optimization" + summary = buildString { + append("Allows dialog boxes to be translated sequentially. ") + append("Avoids problems when loading some translated pages caused by the translator's text formatting. ") + append("Pages will load more slowly.") + } + setDefaultValue(false) + setOnPreferenceChange { _, newValue -> + disableTranslationOptimization = newValue as Boolean + true + } + }.also(screen::addPreference) + } + } + + companion object { + private const val DISABLE_TRANSLATION_OPTIM_PREF = "disableTranslationOptimizationPref" } } diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt index ed9796480..67588fbb9 100644 --- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt +++ b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/SnowmtlFactory.kt @@ -11,9 +11,19 @@ class SnowmtlFactory : SourceFactory { } private val languageList = listOf( - Language("en"), - Language("es"), - Language("id"), - Language("it"), - Language("pt-BR", "pt"), + LanguageSetting("ar", disableSourceSettings = true, disableTranslationOptimization = true), + LanguageSetting("en"), + LanguageSetting("es"), + LanguageSetting("id"), + LanguageSetting("it"), + LanguageSetting("pt-BR", "pt"), ) + +data class LanguageSetting( + override val lang: String, + override val target: String = lang, + override val origin: String = "en", + override var fontSize: Int = 24, + override var disableSourceSettings: Boolean = false, + val disableTranslationOptimization: Boolean = false, +) : Language diff --git a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt index dac36a647..5d288ec0e 100644 --- a/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt +++ b/src/all/snowmtl/src/eu/kanade/tachiyomi/extension/all/snowmtl/interceptors/TranslationInterceptor.kt @@ -2,9 +2,9 @@ package eu.kanade.tachiyomi.extension.all.snowmtl.interceptors import android.os.Build import androidx.annotation.RequiresApi +import eu.kanade.tachiyomi.extension.all.snowmtl.LanguageSetting import eu.kanade.tachiyomi.extension.all.snowmtl.translator.TranslatorEngine import eu.kanade.tachiyomi.multisrc.machinetranslations.Dialog -import eu.kanade.tachiyomi.multisrc.machinetranslations.Language import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations.Companion.PAGE_REGEX import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString @@ -15,7 +15,7 @@ import uy.kohesive.injekt.injectLazy @RequiresApi(Build.VERSION_CODES.O) class TranslationInterceptor( - private val source: Language, + var language: LanguageSetting, private val translator: TranslatorEngine, ) : Interceptor { @@ -25,14 +25,19 @@ class TranslationInterceptor( val request = chain.request() val url = request.url.toString() - if (PAGE_REGEX.containsMatchIn(url).not() || source.target == source.origin) { + if (PAGE_REGEX.containsMatchIn(url).not() || language.target == language.origin) { return chain.proceed(request) } val dialogues = request.url.fragment?.parseAs>() ?: return chain.proceed(request) - val translated = translationOptimized(dialogues) + val translated = when { + language.disableTranslationOptimization -> dialogues.map { + it.replaceText(translator.translate(language.origin, language.target, it.text)) + } + else -> translationOptimized(dialogues) + } val newRequest = request.newBuilder() .url("${url.substringBeforeLast("#")}#${json.encodeToString(translated)}") @@ -52,7 +57,7 @@ class TranslationInterceptor( val mapping = buildMap(dialogues) val tokens = tokenizeAssociatedDialog(mapping).flatMap { token -> - translator.translate(source.origin, source.target, token).split(delimiter) + translator.translate(language.origin, language.target, token).split(delimiter) } return replaceDialoguesWithTranslations(tokens, mapping) @@ -79,13 +84,15 @@ class TranslationInterceptor( val key = list.first() val text = list.last() - mapping[key]?.second?.dialog?.copy( - textByLanguage = mapOf( - "text" to text, - ), - ) + mapping[key]?.second?.dialog?.replaceText(text) } + private fun Dialog.replaceText(value: String) = this.copy( + textByLanguage = mutableMapOf( + "text" to value, + ), + ) + /** * Tokenizes the associated dialogues. * diff --git a/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/Solarmtl.kt b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/Solarmtl.kt index beef89ff1..23f86d7aa 100644 --- a/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/Solarmtl.kt +++ b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/Solarmtl.kt @@ -4,8 +4,6 @@ import android.os.Build import androidx.annotation.RequiresApi import eu.kanade.tachiyomi.multisrc.machinetranslations.Language import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import java.util.concurrent.TimeUnit @RequiresApi(Build.VERSION_CODES.O) class Solarmtl( @@ -14,9 +12,4 @@ class Solarmtl( name = "Solar Machine Translations", baseUrl = "https://solarmtl.com", language, -) { - override val client = super.client.newBuilder() - .connectTimeout(1, TimeUnit.MINUTES) - .rateLimit(2) - .build() -} +) diff --git a/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/SolarmtlFactory.kt b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/SolarmtlFactory.kt index 07cd2ff65..fc801c47e 100644 --- a/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/SolarmtlFactory.kt +++ b/src/all/solarmtl/src/eu/kanade/tachiyomi/extension/all/solarmtl/SolarmtlFactory.kt @@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.extension.all.solarmtl import android.os.Build import androidx.annotation.RequiresApi -import eu.kanade.tachiyomi.multisrc.machinetranslations.Language +import eu.kanade.tachiyomi.multisrc.machinetranslations.LanguageImpl import eu.kanade.tachiyomi.source.SourceFactory @RequiresApi(Build.VERSION_CODES.O) @@ -11,7 +11,7 @@ class SolarmtlFactory : SourceFactory { } private val languageList = listOf( - Language("en"), - Language("fr"), - Language("pt-BR", "pt"), + LanguageImpl("en"), + LanguageImpl("fr"), + LanguageImpl("pt-BR", "pt"), )