Snowmlt: Add support to arabic (#7514)

* Add support to arabic

* Refatoring
This commit is contained in:
Chopper 2025-02-07 12:57:52 -03:00 committed by Draff
parent d6e5553084
commit 643376e8a0
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
11 changed files with 200 additions and 55 deletions

View File

@ -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

View File

@ -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

View File

@ -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<Application>().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)
}

View File

@ -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

View File

@ -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

View File

@ -3,7 +3,7 @@ ext {
extClass = '.SnowmtlFactory'
themePkg = 'machinetranslations'
baseUrl = 'https://snowmtl.ru'
overrideVersionCode = 7
overrideVersionCode = 8
isNsfw = true
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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<List<Dialog>>()
?: 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.
*

View File

@ -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()
}
)

View File

@ -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"),
)