diff --git a/src/all/manhuarm/assets/fonts/LICENSE_ANIMEACE.txt b/src/all/manhuarm/assets/fonts/LICENSE_ANIMEACE.txt new file mode 100644 index 000000000..5ba98714a --- /dev/null +++ b/src/all/manhuarm/assets/fonts/LICENSE_ANIMEACE.txt @@ -0,0 +1,25 @@ +This font is © 2006 Nate Piekos. All Rights Reserved. +Created for Blambot Fonts + +This font is freeware for independent comic book creation and +non-profit use ONLY. ( This excludes use by "mainstream" publishers, +(Marvel, DC, Dark Horse, Oni, Image, SLG, Top Cow, Crossgen and their +subsidiaries) without a license fee. Use by a "mainstream" publisher +(or it's employee), and use for commercial non-comic book production +(eg. magazine ads, merchandise lables etc.) incurs a license fee +be paid to the designer, Nate Piekos. +This font may not be redistributed without the author's permission and +never with this text file missing from the .zip, .sit or .hqx. + +Blambot/Nate Piekos makes no guarantees about these font files, + the completeness of character sets, or safety of these files on your +computer. By installing these fonts on your system, you prove that +you have read and understand the above. + +If you have any questions, visit http://www.blambot.com/license.shtml + +For more free and original fonts visit Blambot. +www.blambot.com + +Nate Piekos +studio@blambot.com diff --git a/src/all/manhuarm/assets/fonts/LICENSE_COMIC_NEUE.txt b/src/all/manhuarm/assets/fonts/LICENSE_COMIC_NEUE.txt new file mode 100644 index 000000000..80062cfab --- /dev/null +++ b/src/all/manhuarm/assets/fonts/LICENSE_COMIC_NEUE.txt @@ -0,0 +1,93 @@ +Copyright 2014 The Comic Neue Project Authors (https://github.com/crozynski/comicneue) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/all/manhuarm/assets/fonts/LICENSE_COMING_SOON.txt b/src/all/manhuarm/assets/fonts/LICENSE_COMING_SOON.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/src/all/manhuarm/assets/fonts/LICENSE_COMING_SOON.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/all/manhuarm/assets/fonts/animeace2_regular.ttf b/src/all/manhuarm/assets/fonts/animeace2_regular.ttf new file mode 100644 index 000000000..6521ab5b5 Binary files /dev/null and b/src/all/manhuarm/assets/fonts/animeace2_regular.ttf differ diff --git a/src/all/manhuarm/assets/fonts/comic_neue_bold.ttf b/src/all/manhuarm/assets/fonts/comic_neue_bold.ttf new file mode 100644 index 000000000..dc9a6d552 Binary files /dev/null and b/src/all/manhuarm/assets/fonts/comic_neue_bold.ttf differ diff --git a/src/all/manhuarm/assets/fonts/comic_neue_regular.ttf b/src/all/manhuarm/assets/fonts/comic_neue_regular.ttf new file mode 100644 index 000000000..d454f4693 Binary files /dev/null and b/src/all/manhuarm/assets/fonts/comic_neue_regular.ttf differ diff --git a/src/all/manhuarm/assets/fonts/coming_soon_regular.ttf b/src/all/manhuarm/assets/fonts/coming_soon_regular.ttf new file mode 100644 index 000000000..b0f6908dc Binary files /dev/null and b/src/all/manhuarm/assets/fonts/coming_soon_regular.ttf differ diff --git a/src/all/manhuarm/assets/i18n/messages_manhuarm_en.properties b/src/all/manhuarm/assets/i18n/messages_manhuarm_en.properties new file mode 100644 index 000000000..779fbe9e3 --- /dev/null +++ b/src/all/manhuarm/assets/i18n/messages_manhuarm_en.properties @@ -0,0 +1,19 @@ +font_name_title=Font name +font_name_summary=Customize dialogues by choosing your preferred font. To apply the change to chapters that have already been read, you will need to clear the chapter cache in "More > Data and storage > Clear chapter cache". Font changes will not affect downloaded chapters +font_name_message=Font name changed to %s +font_name_device_title=Device +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 +default_font_name=Default +disable_word_break_title=Disable word break +disable_word_break_summary=This feature prevents words from being automatically broken in the middle of a line. +disable_translator_title=Disable translator +disable_translator_summary=Disable auto translation and enable source translation. This does not mean that a translation is available. Make sure a translation is available before enabling this option. +translate_dialog_box_title=Translator +translate_dialog_box_summary=Engine used to translate dialog boxes +translate_dialog_box_toast=The translator has been changed to +enable_manga_details_translation_title=Enable translation of manga details +enable_manga_details_translation_summary=This option will slow down the loading of manga details +chapter_unavailable_message=Chapter cannot be translated diff --git a/src/all/manhuarm/assets/i18n/messages_manhuarm_es.properties b/src/all/manhuarm/assets/i18n/messages_manhuarm_es.properties new file mode 100644 index 000000000..48bc523f9 --- /dev/null +++ b/src/all/manhuarm/assets/i18n/messages_manhuarm_es.properties @@ -0,0 +1 @@ +font_size_title=Tamaño de letra diff --git a/src/all/manhuarm/assets/i18n/messages_manhuarm_fr.properties b/src/all/manhuarm/assets/i18n/messages_manhuarm_fr.properties new file mode 100644 index 000000000..1beb3574f --- /dev/null +++ b/src/all/manhuarm/assets/i18n/messages_manhuarm_fr.properties @@ -0,0 +1 @@ +font_size_title=Taille de la police diff --git a/src/all/manhuarm/assets/i18n/messages_manhuarm_id.properties b/src/all/manhuarm/assets/i18n/messages_manhuarm_id.properties new file mode 100644 index 000000000..b129b31a4 --- /dev/null +++ b/src/all/manhuarm/assets/i18n/messages_manhuarm_id.properties @@ -0,0 +1 @@ +font_size_title=Ukuran font diff --git a/src/all/manhuarm/assets/i18n/messages_manhuarm_it.properties b/src/all/manhuarm/assets/i18n/messages_manhuarm_it.properties new file mode 100644 index 000000000..917acd0f9 --- /dev/null +++ b/src/all/manhuarm/assets/i18n/messages_manhuarm_it.properties @@ -0,0 +1 @@ +font_size_title=Dimensione del carattere diff --git a/src/all/manhuarm/assets/i18n/messages_manhuarm_pt_br.properties b/src/all/manhuarm/assets/i18n/messages_manhuarm_pt_br.properties new file mode 100644 index 000000000..b58cd28c0 --- /dev/null +++ b/src/all/manhuarm/assets/i18n/messages_manhuarm_pt_br.properties @@ -0,0 +1,19 @@ +font_name_title=Fonte +font_name_summary=Personalize os diálogos escolhendo a fonte de sua preferência. Para aplicar a alteração em capitulos lidos será necessário limpar o cache de capitulos em "Mais > Dados e armazenamento > Limpar cache de capítulos". As alterações de fonte não serão aplicadas aos capítulos baixados. +font_name_message=Fonte alterada para %s +font_name_device_title=Dispositivo +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 +default_font_name=Padrão +disable_word_break_title=Desativar quebra de palavras +disable_word_break_summary=Esse recurso impede que palavras sejam quebradas automaticamente no meio da linha. +disable_translator_title=Desativar tradutor +disable_translator_summary=Desativar tradução automática e ativar tradução da fonte. Isso não significa que há uma tradução disponivel. Certifique-se de que exista uma tradução antes de ativar esta opção no site. +translate_dialog_box_title=Tradutor +translate_dialog_box_summary=Motor utilizado para traduzir caixas de diálogo +translate_dialog_box_toast=O tradutor foi alterado para +enable_manga_details_translation_title=Habilitar tradução dos detalhes do manga +enable_manga_details_translation_summary=Esta opção tornará o carregamento dos detalhes do mangá mais lento +chapter_unavailable_message=O capítulo não pode ser traduzido diff --git a/src/en/manhuarm/build.gradle b/src/all/manhuarm/build.gradle similarity index 70% rename from src/en/manhuarm/build.gradle rename to src/all/manhuarm/build.gradle index 7775dad94..2a7aee99d 100644 --- a/src/en/manhuarm/build.gradle +++ b/src/all/manhuarm/build.gradle @@ -1,9 +1,9 @@ ext { extName = 'Manhuarm' - extClass = '.Manhuarm' + extClass = '.ManhuarmFactory' themePkg = 'madara' baseUrl = 'https://manhuarm.com' - overrideVersionCode = 0 + overrideVersionCode = 1 isNsfw = true } diff --git a/src/en/manhuarm/res/mipmap-hdpi/ic_launcher.png b/src/all/manhuarm/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/en/manhuarm/res/mipmap-hdpi/ic_launcher.png rename to src/all/manhuarm/res/mipmap-hdpi/ic_launcher.png diff --git a/src/en/manhuarm/res/mipmap-mdpi/ic_launcher.png b/src/all/manhuarm/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/en/manhuarm/res/mipmap-mdpi/ic_launcher.png rename to src/all/manhuarm/res/mipmap-mdpi/ic_launcher.png diff --git a/src/en/manhuarm/res/mipmap-xhdpi/ic_launcher.png b/src/all/manhuarm/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/en/manhuarm/res/mipmap-xhdpi/ic_launcher.png rename to src/all/manhuarm/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/en/manhuarm/res/mipmap-xxhdpi/ic_launcher.png b/src/all/manhuarm/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/en/manhuarm/res/mipmap-xxhdpi/ic_launcher.png rename to src/all/manhuarm/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/en/manhuarm/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/manhuarm/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/en/manhuarm/res/mipmap-xxxhdpi/ic_launcher.png rename to src/all/manhuarm/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/Manhuarm.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/Manhuarm.kt new file mode 100644 index 000000000..4db837e76 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/Manhuarm.kt @@ -0,0 +1,353 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm + +import android.content.SharedPreferences +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.extension.all.manhuarm.interceptors.ComposedImageInterceptor +import eu.kanade.tachiyomi.extension.all.manhuarm.interceptors.TranslationInterceptor +import eu.kanade.tachiyomi.extension.all.manhuarm.translator.bing.BingTranslator +import eu.kanade.tachiyomi.extension.all.manhuarm.translator.google.GoogleTranslator +import eu.kanade.tachiyomi.lib.i18n.Intl +import eu.kanade.tachiyomi.lib.i18n.Intl.Companion.createDefaultMessageFileName +import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine +import eu.kanade.tachiyomi.multisrc.madara.Madara +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import keiyoushi.utils.getPreferencesLazy +import keiyoushi.utils.parseAs +import kotlinx.serialization.encodeToString +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response +import org.jsoup.nodes.Document +import java.util.Calendar +import java.util.Date +import java.util.concurrent.TimeUnit + +@RequiresApi(Build.VERSION_CODES.O) +class Manhuarm( + private val language: Language, +) : Madara( + "Manhuarm", + "https://manhuarm.com", + language.lang, +), + ConfigurableSource { + + override val useNewChapterEndpoint: Boolean = true + + private val preferences: SharedPreferences by getPreferencesLazy() + + /** + * 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 + } + + private var fontSize: Int + get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt() + set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply() + + private var fontName: String + get() = preferences.getString(FONT_NAME_PREF, language.fontName)!! + set(value) = preferences.edit().putString(FONT_NAME_PREF, value).apply() + + private var disableWordBreak: Boolean + get() = preferences.getBoolean(DISABLE_WORD_BREAK_PREF, language.disableWordBreak) + set(value) = preferences.edit().putBoolean(DISABLE_WORD_BREAK_PREF, value).apply() + + private var disableTranslator: Boolean + get() = preferences.getBoolean(DISABLE_TRANSLATOR_PREF, language.disableTranslator) + set(value) = preferences.edit().putBoolean(DISABLE_TRANSLATOR_PREF, value).apply() + + private val i18n = Intl( + language = language.lang, + baseLanguage = "en", + availableLanguages = setOf("en", "es", "fr", "id", "it", "pt-BR"), + classLoader = this::class.java.classLoader!!, + createMessageFileName = { createDefaultMessageFileName("${name.lowercase()}_${language.lang}") }, + ) + + private val settings get() = language.copy( + fontSize = this@Manhuarm.fontSize, + fontName = this@Manhuarm.fontName, + disableWordBreak = this@Manhuarm.disableWordBreak, + disableTranslator = this@Manhuarm.disableTranslator, + disableFontSettings = this@Manhuarm.fontName == DEVICE_FONT, + ) + + override val client: OkHttpClient get() = clientInstance!! + + private val translators = arrayOf( + "Bing", + "Google", + ) + + private val provider: String get() = + preferences.getString(TRANSLATOR_PROVIDER_PREF, translators.first())!! + + /** + * 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 + } + private val clientUtils = network.cloudflareClient.newBuilder() + .rateLimit(3, 2, TimeUnit.SECONDS) + .build() + + private lateinit var translator: TranslatorEngine + + private fun clientBuilder(): OkHttpClient.Builder { + translator = when (provider) { + "Google" -> GoogleTranslator(clientUtils, headers) + else -> BingTranslator(clientUtils, headers) + } + + return network.cloudflareClient.newBuilder() + .connectTimeout(1, TimeUnit.MINUTES) + .readTimeout(2, TimeUnit.MINUTES) + .rateLimit(3) + .addInterceptorIf( + !disableTranslator && language.lang != language.origin, + TranslationInterceptor(settings, translator), + ) + .addInterceptor(ComposedImageInterceptor(settings)) + } + + private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder { + return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor) + } + + private val translationAvailability = Calendar.getInstance().apply { + set(2025, Calendar.SEPTEMBER, 9, 0, 0, 0) + set(Calendar.MILLISECOND, 0) + } + + override fun chapterListParse(response: Response): List { + return super.chapterListParse(response).filter { + language.target == language.origin || Date(it.date_upload).after(translationAvailability.time) + } + } + + override fun pageListParse(document: Document): List { + val pages = super.pageListParse(document) + val content = document.selectFirst("meta[name=description]") + ?.attr("content") + ?.fixJsonFormat() + ?: return pages.takeIf { language.target == language.origin } ?: throw Exception(i18n["chapter_unavailable_message"]) + + val dialog = content.parseAs>() + + return dialog.mapIndexed { index, dto -> + val page = pages.first { it.imageUrl?.contains(dto.imageUrl, true)!! } + val fragment = json.encodeToString>( + dto.dialogues.filter { it.getTextBy(language).isNotBlank() }, + ) + if (dto.dialogues.isEmpty()) { + return@mapIndexed page + } + + Page(index, imageUrl = "${page.imageUrl}${fragment.toFragment()}") + } + } + + private fun String.fixJsonFormat(): String { + return JSON_FORMAT_REGEX.replace(this) { matchResult -> + val content = matchResult.groupValues.last() + val modifiedContent = content.replace("\"", "'") + """"text": "${modifiedContent.trimIndent()}", "box"""" + } + } + + // Prevent bad fragments + fun String.toFragment(): String = "#${this.replace("#", "*")}" + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + // Some libreoffice font sizes + val sizes = arrayOf( + "12", "13", "14", + "15", "16", "18", + "20", "21", "22", + "24", "26", "28", + "32", "36", "40", + "42", "44", "48", + "54", "60", "72", + "80", "88", "96", + ) + + val fonts = arrayOf( + i18n["font_name_device_title"] to DEVICE_FONT, + "Anime Ace" to "animeace2_regular", + "Comic Neue" to "comic_neue_bold", + "Coming Soon" to "coming_soon_regular", + ) + + ListPreference(screen.context).apply { + key = FONT_SIZE_PREF + title = i18n["font_size_title"] + entries = sizes.map { + "${it}pt" + if (it == DEFAULT_FONT_SIZE) " - ${i18n["default_font_size"]}" else "" + }.toTypedArray() + entryValues = sizes + + summary = buildString { + appendLine(i18n["font_size_summary"]) + append("\t* %s") + } + + setDefaultValue(fontSize.toString()) + + setOnPreferenceChange { _, newValue -> + val selected = newValue as String + val index = this.findIndexOfValue(selected) + val entry = entries[index] as String + + fontSize = selected.toInt() + + Toast.makeText( + screen.context, + i18n["font_size_message"].format(entry), + Toast.LENGTH_LONG, + ).show() + + true // It's necessary to update the user interface + } + }.also(screen::addPreference) + + if (!language.disableFontSettings) { + ListPreference(screen.context).apply { + key = FONT_NAME_PREF + title = i18n["font_name_title"] + entries = fonts.map { + it.first + if (it.second.isBlank()) " - ${i18n["default_font_name"]}" else "" + }.toTypedArray() + entryValues = fonts.map { it.second }.toTypedArray() + summary = buildString { + appendLine(i18n["font_name_summary"]) + append("\t* %s") + } + + setDefaultValue(fontName) + + setOnPreferenceChange { _, newValue -> + val selected = newValue as String + val index = this.findIndexOfValue(selected) + val entry = entries[index] as String + + fontName = selected + + Toast.makeText( + screen.context, + i18n["font_name_message"].format(entry), + Toast.LENGTH_LONG, + ).show() + + true // It's necessary to update the user interface + } + }.also(screen::addPreference) + } + + SwitchPreferenceCompat(screen.context).apply { + key = DISABLE_WORD_BREAK_PREF + title = "⚠ ${i18n["disable_word_break_title"]}" + summary = i18n["disable_word_break_summary"] + setDefaultValue(language.disableWordBreak) + setOnPreferenceChange { _, newValue -> + disableWordBreak = newValue as Boolean + true + } + }.also(screen::addPreference) + + if (language.target == language.origin) { + return + } + + if (language.supportNativeTranslation) { + SwitchPreferenceCompat(screen.context).apply { + key = DISABLE_TRANSLATOR_PREF + title = "⚠ ${i18n["disable_translator_title"]}" + summary = i18n["disable_translator_summary"] + setDefaultValue(language.disableTranslator) + setOnPreferenceChange { _, newValue -> + disableTranslator = newValue as Boolean + true + } + }.also(screen::addPreference) + } + + if (!disableTranslator) { + ListPreference(screen.context).apply { + key = TRANSLATOR_PROVIDER_PREF + title = i18n["translate_dialog_box_title"] + entries = translators + entryValues = translators + summary = buildString { + appendLine(i18n["translate_dialog_box_summary"]) + append("\t* %s") + } + + setDefaultValue(translators.first()) + + setOnPreferenceChange { _, newValue -> + val selected = newValue as String + val index = this.findIndexOfValue(selected) + val entry = entries[index] as String + + Toast.makeText( + screen.context, + "${i18n["translate_dialog_box_toast"]} '$entry'", + Toast.LENGTH_LONG, + ).show() + + 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. + */ + private 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) + val JSON_FORMAT_REGEX = """(?:"text":\s+?".*?)([\s\S]*?)(?:",\s+?"box")""".toRegex() + + const val DEVICE_FONT = "device:" + private const val FONT_SIZE_PREF = "fontSizePref" + private const val FONT_NAME_PREF = "fontNamePref" + private const val DISABLE_WORD_BREAK_PREF = "disableWordBreakPref" + private const val DISABLE_TRANSLATOR_PREF = "disableTranslatorPref" + private const val TRANSLATOR_PROVIDER_PREF = "translatorProviderPref" + private const val DEFAULT_FONT_SIZE = "28" + } +} diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmDto.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmDto.kt new file mode 100644 index 000000000..9ba189472 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmDto.kt @@ -0,0 +1,95 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm + +import android.os.Build +import androidx.annotation.RequiresApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonTransformingSerializer +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put +import java.io.IOException + +@Serializable +class PageDto( + @SerialName("image") + val imageUrl: String, + + @SerialName("texts") + @Serializable(with = DialogListSerializer::class) + val dialogues: List = emptyList(), +) + +@Serializable +@RequiresApi(Build.VERSION_CODES.O) +data class Dialog( + val x: Float, + val y: Float, + val width: Float, + val height: Float, + val angle: Float = 0f, + val isBold: Boolean = false, + val isNewApi: Boolean = false, + val textByLanguage: Map = emptyMap(), + val type: String = "normal", + private val fbColor: List = emptyList(), + private val bgColor: List = emptyList(), +) { + val text: String get() = textByLanguage["text"] ?: throw Exception("Dialog not found") + fun getTextBy(language: Language) = when { + !language.disableTranslator -> textByLanguage[language.origin] + else -> textByLanguage[language.target] + } ?: text + val centerY get() = height / 2 + y + val centerX get() = width / 2 + x +} + +private object DialogListSerializer : + JsonTransformingSerializer>(ListSerializer(Dialog.serializer())) { + override fun transformDeserialize(element: JsonElement): JsonElement { + return JsonArray( + element.jsonArray.map { jsonElement -> + val coordinates = getCoordinates(jsonElement) + val textByLanguage = getDialogs(jsonElement) + + buildJsonObject { + put("x", coordinates[0]) + put("y", coordinates[1]) + put("width", coordinates[2]) + put("height", coordinates[3]) + put("textByLanguage", textByLanguage) + } + }, + ) + } + + private fun getCoordinates(element: JsonElement): JsonArray { + return when (element) { + is JsonArray -> element.jsonArray[0].jsonArray + else -> element.jsonObject["box"]?.jsonArray + ?: throw IOException("Dialog box position not found") + } + } + private fun getDialogs(element: JsonElement): JsonObject { + return buildJsonObject { + when (element) { + is JsonArray -> put("text", element.jsonArray[1]) + else -> { + element.jsonObject.entries + .filter { it.value.isString } + .forEach { put(it.key, it.value) } + } + } + } + } + + private val JsonElement.isArray get() = this is JsonArray + private val JsonElement.isObject get() = this is JsonObject + private val JsonElement.isString get() = this.isObject.not() && this.isArray.not() && this.jsonPrimitive.isString +} diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmFactory.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmFactory.kt new file mode 100644 index 000000000..35f3b88fd --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmFactory.kt @@ -0,0 +1,20 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm + +import android.os.Build +import androidx.annotation.RequiresApi +import eu.kanade.tachiyomi.source.SourceFactory + +@RequiresApi(Build.VERSION_CODES.O) +class ManhuarmFactory : SourceFactory { + override fun createSources() = languageList.map(::Manhuarm) +} + +private val languageList = listOf( + Language("ar", disableFontSettings = true), + Language("en"), + Language("es"), + Language("fr", supportNativeTranslation = true), + Language("id", supportNativeTranslation = true), + Language("it"), + Language("pt-BR", "pt", supportNativeTranslation = true), +) diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmUtils.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmUtils.kt new file mode 100644 index 000000000..aea5af653 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/ManhuarmUtils.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm + +class MachineTranslationsFactoryUtils + +data class Language( + val lang: String, + val target: String = lang, + val origin: String = "en", + val fontSize: Int = 28, + val disableFontSettings: Boolean = false, + val disableWordBreak: Boolean = false, + val disableTranslator: Boolean = false, + val supportNativeTranslation: Boolean = false, + val fontName: String = "comic_neue_bold", +) diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/interceptors/ComposedImageInterceptor.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/interceptors/ComposedImageInterceptor.kt new file mode 100644 index 000000000..d12b55c37 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/interceptors/ComposedImageInterceptor.kt @@ -0,0 +1,221 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm.interceptors + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Typeface +import android.graphics.text.LineBreaker +import android.os.Build +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import androidx.annotation.RequiresApi +import eu.kanade.tachiyomi.extension.all.manhuarm.Dialog +import eu.kanade.tachiyomi.extension.all.manhuarm.Language +import eu.kanade.tachiyomi.extension.all.manhuarm.Manhuarm.Companion.PAGE_REGEX +import keiyoushi.utils.parseAs +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream + +// The Interceptor joins the dialogues and pages of the manga. +@RequiresApi(Build.VERSION_CODES.O) +class ComposedImageInterceptor( + val language: Language, +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url.toString() + + if (PAGE_REGEX.containsMatchIn(url).not()) { + return chain.proceed(request) + } + + val dialogues = request.url.fragment?.parseAs>() + ?: emptyList() + + val imageRequest = request.newBuilder() + .url(url) + .build() + + val response = chain.proceed(imageRequest) + + if (response.isSuccessful.not()) { + return response + } + + val bitmap = BitmapFactory.decodeStream(response.body.byteStream())!! + .copy(Bitmap.Config.ARGB_8888, true) + + val canvas = Canvas(bitmap) + + dialogues.forEach { dialog -> + val textPaint = createTextPaint(selectFontFamily()) + val dialogBox = createDialogBox(dialog, textPaint) + val y = getYAxis(textPaint, dialog, dialogBox) + canvas.draw(textPaint, dialogBox, dialog, dialog.x, y) + } + + val output = ByteArrayOutputStream() + + val ext = url.substringBefore("#") + .substringAfterLast(".") + .lowercase() + val format = when (ext) { + "png" -> Bitmap.CompressFormat.PNG + "jpeg", "jpg" -> Bitmap.CompressFormat.JPEG + else -> Bitmap.CompressFormat.WEBP + } + + bitmap.compress(format, 100, output) + + val responseBody = output.toByteArray().toResponseBody(mediaType) + + return response.newBuilder() + .body(responseBody) + .build() + } + + private fun createTextPaint(font: Typeface?): TextPaint { + val defaultTextSize = language.fontSize.pt + return TextPaint().apply { + color = Color.BLACK + textSize = defaultTextSize + font?.let { + typeface = it + } + isAntiAlias = true + } + } + + private fun selectFontFamily(): Typeface? { + if (language.disableFontSettings) { + return null + } + return loadFont("${language.fontName}.ttf") + } + + /** + * Loads font from the `assets/fonts` directory within the APK + * + * @param fontName The name of the font to load. + * @return A `Typeface` instance of the loaded font or `null` if an error occurs. + * + * Example usage: + *
{@code
+     *   val typeface: TypeFace? = loadFont("filename.ttf")
+     * }
+ */ + private fun loadFont(fontName: String): Typeface? { + return try { + this::class.java.classLoader!! + .getResourceAsStream("assets/fonts/$fontName") + .toTypeface(fontName) + } catch (e: Exception) { + null + } + } + + private fun InputStream.toTypeface(fontName: String): Typeface? { + val fontFile = File.createTempFile(fontName, fontName.substringAfter(".")) + this.copyTo(FileOutputStream(fontFile)) + return Typeface.createFromFile(fontFile) + } + + /** + * Adjust the text to the center of the dialog box when feasible. + */ + private fun getYAxis(textPaint: TextPaint, dialog: Dialog, dialogBox: StaticLayout): Float { + val fontHeight = textPaint.fontMetrics.let { it.bottom - it.top } + + val dialogBoxLineCount = dialog.height / fontHeight + + /** + * Centers text in y for dialogues smaller than the dialog box + */ + return when { + dialogBox.lineCount < dialogBoxLineCount -> dialog.centerY - dialogBox.lineCount / 2f * fontHeight + else -> dialog.y + } + } + + private fun createDialogBox(dialog: Dialog, textPaint: TextPaint): StaticLayout { + var dialogBox = createBoxLayout(dialog, textPaint) + + /** + * The best way I've found to adjust the text in the dialog box (Especially in long dialogues) + */ + while (dialogBox.height > dialog.height) { + textPaint.textSize -= 0.5f + dialogBox = createBoxLayout(dialog, textPaint) + } + + textPaint.color = Color.BLACK + textPaint.bgColor = Color.WHITE + + return dialogBox + } + + private fun createBoxLayout(dialog: Dialog, textPaint: TextPaint): StaticLayout { + val text = dialog.getTextBy(language) + + return StaticLayout.Builder.obtain(text, 0, text.length, textPaint, dialog.width.toInt()).apply { + setAlignment(Layout.Alignment.ALIGN_CENTER) + setIncludePad(language.disableFontSettings) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (language.disableWordBreak) { + setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE) + setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE) + return@apply + } + setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED) + setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL) + } + }.build() + } + + private fun Canvas.draw(textPaint: TextPaint, layout: StaticLayout, dialog: Dialog, x: Float, y: Float) { + save() + translate(x, y) + rotate(dialog.angle) + drawTextOutline(textPaint, layout) + drawText(textPaint, layout) + restore() + } + + private fun Canvas.drawText(textPaint: TextPaint, layout: StaticLayout) { + textPaint.style = Paint.Style.FILL + layout.draw(this) + } + + private fun Canvas.drawTextOutline(textPaint: TextPaint, layout: StaticLayout) { + val foregroundColor = textPaint.color + val style = textPaint.style + + textPaint.strokeWidth = 5F + textPaint.color = textPaint.bgColor + textPaint.style = Paint.Style.FILL_AND_STROKE + + layout.draw(this) + + textPaint.color = foregroundColor + textPaint.style = style + } + + // https://pixelsconverter.com/pt-to-px + private val Int.pt: Float get() = this / SCALED_DENSITY + + companion object { + // w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths) + const val SCALED_DENSITY = 0.75f // 1px = 0.75pt + val mediaType = "image/png".toMediaType() + } +} diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/interceptors/TranslationInterceptor.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/interceptors/TranslationInterceptor.kt new file mode 100644 index 000000000..2c91f1e25 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/interceptors/TranslationInterceptor.kt @@ -0,0 +1,61 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm.interceptors + +import android.os.Build +import androidx.annotation.RequiresApi +import eu.kanade.tachiyomi.extension.all.manhuarm.Dialog +import eu.kanade.tachiyomi.extension.all.manhuarm.Language +import eu.kanade.tachiyomi.extension.all.manhuarm.Manhuarm.Companion.PAGE_REGEX +import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine +import keiyoushi.utils.parseAs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.Response +import uy.kohesive.injekt.injectLazy + +@RequiresApi(Build.VERSION_CODES.O) +class TranslationInterceptor( + val language: Language, + private val translator: TranslatorEngine, +) : Interceptor { + + private val json: Json by injectLazy() + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val url = request.url.toString() + + 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 = runBlocking(Dispatchers.IO) { + dialogues.map { dialog -> + async { + dialog.replaceText( + translator.translate(language.origin, language.target, dialog.text), + ) + } + }.awaitAll() + } + + val newRequest = request.newBuilder() + .url("${url.substringBeforeLast("#")}#${json.encodeToString(translated)}") + .build() + + return chain.proceed(newRequest) + } + + private fun Dialog.replaceText(value: String) = this.copy( + textByLanguage = mutableMapOf( + "text" to value, + ), + ) +} diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/TranslatorEngine.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/TranslatorEngine.kt new file mode 100644 index 000000000..d8a420361 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/TranslatorEngine.kt @@ -0,0 +1,6 @@ +package eu.kanade.tachiyomi.multisrc.machinetranslations.translator + +interface TranslatorEngine { + val capacity: Int + fun translate(from: String, to: String, text: String): String +} diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/bing/BingTranslator.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/bing/BingTranslator.kt new file mode 100644 index 000000000..b5dbdbaee --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/bing/BingTranslator.kt @@ -0,0 +1,115 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm.translator.bing + +import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Element +import uy.kohesive.injekt.injectLazy + +class BingTranslator(private val client: OkHttpClient, private val headers: Headers) : TranslatorEngine { + + private val baseUrl = "https://www.bing.com" + + private val translatorUrl = "$baseUrl/translator" + + private val json: Json by injectLazy() + + private var tokens: TokenGroup = TokenGroup() + + override val capacity: Int = 1000 + + private val attempts = 3 + + override fun translate(from: String, to: String, text: String): String { + if (tokens.isNotValid() && refreshTokens().not()) { + return text + } + val request = translatorRequest(from, to, text) + repeat(attempts) { + try { + return fetchTranslatedText(request) + } catch (e: Exception) { + refreshTokens() + } + } + return text + } + + private fun fetchTranslatedText(request: Request): String { + return client.newCall(request).execute().parseAs>() + .firstOrNull()!!.text + } + + private fun refreshTokens(): Boolean { + tokens = loadTokens() + return tokens.isValid() + } + + private fun translatorRequest(from: String, to: String, text: String): Request { + val url = "$baseUrl/ttranslatev3".toHttpUrl().newBuilder() + .addQueryParameter("isVertical", "1") + .addQueryParameter("", "") // Present in Bing URL + .addQueryParameter("IG", tokens.ig) + .addQueryParameter("IID", tokens.iid) + .build() + + val headersApi = headers.newBuilder() + .set("Accept", "*/*") + .set("Origin", baseUrl) + .set("Referer", translatorUrl) + .set("Alt-Used", baseUrl) + .build() + + val payload = FormBody.Builder() + .add("fromLang", from) + .add("to", to) + .add("text", text) + .add("tryFetchingGenderDebiasedTranslations", "true") + .add("token", tokens.token) + .add("key", tokens.key) + .build() + + return POST(url.toString(), headersApi, payload) + } + + private fun loadTokens(): TokenGroup { + val document = client.newCall(GET(translatorUrl, headers)).execute().asJsoup() + + val scripts = document.select("script") + .map(Element::data) + + val scriptOne: String = scripts.firstOrNull(TOKENS_REGEX::containsMatchIn) + ?: return TokenGroup() + + val scriptTwo: String = scripts.firstOrNull(IG_PARAM_REGEX::containsMatchIn) + ?: return TokenGroup() + + val matchOne = TOKENS_REGEX.find(scriptOne)?.groups + val matchTwo = IG_PARAM_REGEX.find(scriptTwo)?.groups + + return TokenGroup( + token = matchOne?.get(4)?.value ?: "", + key = matchOne?.get(3)?.value ?: "", + ig = matchTwo?.get(1)?.value ?: "", + iid = document.selectFirst("div[data-iid]:not([class])")?.attr("data-iid") ?: "", + ) + } + + private inline fun Response.parseAs(): T { + return json.decodeFromStream(body.byteStream()) + } + + companion object { + val TOKENS_REGEX = """params_AbusePreventionHelper(\s+)?=(\s+)?[^\[]\[(\d+),"([^"]+)""".toRegex() + val IG_PARAM_REGEX = """IG:"([^"]+)""".toRegex() + } +} diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/bing/BingTranslatorDto.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/bing/BingTranslatorDto.kt new file mode 100644 index 000000000..6d204464f --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/bing/BingTranslatorDto.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm.translator.bing + +import kotlinx.serialization.Serializable + +class BingTranslatorDto + +class TokenGroup( + val token: String = "", + val key: String = "", + val iid: String = "", + val ig: String = "", +) { + fun isNotValid() = listOf(token, key, iid, ig).any(String::isBlank) + + fun isValid() = isNotValid().not() +} + +@Serializable +class TranslateDto( + val translations: List, +) { + val text = translations.firstOrNull()?.text ?: "" +} + +@Serializable +class TextTranslated( + val text: String, +) diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/google/GoogleTranslator.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/google/GoogleTranslator.kt new file mode 100644 index 000000000..6056cdb67 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/google/GoogleTranslator.kt @@ -0,0 +1,84 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm.translator.google + +import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine +import eu.kanade.tachiyomi.network.GET +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.injectLazy +import java.io.IOException + +/** + * This client is an adaptation of the following python repository: https://github.com/ssut/py-googletrans. + */ +class GoogleTranslator(private val client: OkHttpClient, private val headers: Headers) : TranslatorEngine { + + private val baseUrl: String = "https://translate.googleapis.com" + + private val webpage: String = "https://translate.google.com" + + private val translatorUrl = "$baseUrl/translate_a/single" + + override val capacity: Int = 5000 + + private val json: Json by injectLazy() + + override fun translate(from: String, to: String, text: String): String { + val request = translateRequest(text, from, to) + return try { fetchTranslatedText(request) } catch (_: Exception) { text } + } + + private fun translateRequest(text: String, from: String, to: String): Request { + return GET(clientUrlBuilder(text, from, to).build(), headersBuilder().build()) + } + + private fun headersBuilder(): Headers.Builder = headers.newBuilder() + .set("Origin", webpage) + .set("Alt-Used", webpage.substringAfterLast("/")) + .set("Referer", "$webpage/") + + private fun clientUrlBuilder(text: String, src: String, dest: String, token: String = "xxxx"): HttpUrl.Builder { + return translatorUrl.toHttpUrl().newBuilder() + .setQueryParameter("client", "gtx") + .setQueryParameter("sl", src) + .setQueryParameter("tl", dest) + .setQueryParameter("hl", dest) + .setQueryParameter("ie", Charsets.UTF_8.toString()) + .setQueryParameter("oe", Charsets.UTF_8.toString()) + .setQueryParameter("otf", "1") + .setQueryParameter("ssel", "0") + .setQueryParameter("tsel", "0") + .setQueryParameter("tk", token) + .setQueryParameter("q", text) + .apply { + arrayOf("at", "bd", "ex", "ld", "md", "qca", "rw", "rm", "ss", "t").forEach { + addQueryParameter("dt", it) + } + } + } + + private fun fetchTranslatedText(request: Request): String { + val response = client.newCall(request).execute() + + if (response.isSuccessful.not()) { + throw IOException("Request failed: ${response.code}") + } + + return response.parseJson().let(::extractTranslatedText) + } + + private fun Response.parseJson(): JsonElement = json.parseToJsonElement(this.body.string()) + + private fun extractTranslatedText(data: JsonElement): String { + return data.jsonArray[0].jsonArray.joinToString("") { + it.jsonArray[0].jsonPrimitive.content + } + } +} diff --git a/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/google/GoogleTranslatorDto.kt b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/google/GoogleTranslatorDto.kt new file mode 100644 index 000000000..d6a4dedb9 --- /dev/null +++ b/src/all/manhuarm/src/eu/kanade/tachiyomi/extension/all/manhuarm/translator/google/GoogleTranslatorDto.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.extension.all.manhuarm.translator.google + +import okhttp3.Response + +class GoogleTranslatorDto + +data class Translated( + val from: String, + val to: String, + val origin: String, + val text: String, + val pronunciation: String, + val extraData: Map, + val response: Response, +) diff --git a/src/en/manhuarm/src/eu/kanade/tachiyomi/extension/en/manhuarm/Manhuarm.kt b/src/en/manhuarm/src/eu/kanade/tachiyomi/extension/en/manhuarm/Manhuarm.kt deleted file mode 100644 index 33b801810..000000000 --- a/src/en/manhuarm/src/eu/kanade/tachiyomi/extension/en/manhuarm/Manhuarm.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.manhuarm - -import eu.kanade.tachiyomi.multisrc.madara.Madara - -class Manhuarm : Madara( - "Manhuarm", - "https://manhuarm.com", - "en", -) { - override val useNewChapterEndpoint: Boolean = true -}