Manhuarm: Fix dialog box empty and add translation support (#10625)
Fix dialog box empty and add translation support
25
src/all/manhuarm/assets/fonts/LICENSE_ANIMEACE.txt
Normal file
@ -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
|
93
src/all/manhuarm/assets/fonts/LICENSE_COMIC_NEUE.txt
Normal file
@ -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.
|
202
src/all/manhuarm/assets/fonts/LICENSE_COMING_SOON.txt
Normal file
@ -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.
|
BIN
src/all/manhuarm/assets/fonts/animeace2_regular.ttf
Normal file
BIN
src/all/manhuarm/assets/fonts/comic_neue_bold.ttf
Normal file
BIN
src/all/manhuarm/assets/fonts/comic_neue_regular.ttf
Normal file
BIN
src/all/manhuarm/assets/fonts/coming_soon_regular.ttf
Normal file
19
src/all/manhuarm/assets/i18n/messages_manhuarm_en.properties
Normal file
@ -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
|
@ -0,0 +1 @@
|
||||
font_size_title=Tamaño de letra
|
@ -0,0 +1 @@
|
||||
font_size_title=Taille de la police
|
@ -0,0 +1 @@
|
||||
font_size_title=Ukuran font
|
@ -0,0 +1 @@
|
||||
font_size_title=Dimensione del carattere
|
@ -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
|
@ -1,9 +1,9 @@
|
||||
ext {
|
||||
extName = 'Manhuarm'
|
||||
extClass = '.Manhuarm'
|
||||
extClass = '.ManhuarmFactory'
|
||||
themePkg = 'madara'
|
||||
baseUrl = 'https://manhuarm.com'
|
||||
overrideVersionCode = 0
|
||||
overrideVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@ -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<SChapter> {
|
||||
return super.chapterListParse(response).filter {
|
||||
language.target == language.origin || Date(it.date_upload).after(translationAvailability.time)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
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<List<PageDto>>()
|
||||
|
||||
return dialog.mapIndexed { index, dto ->
|
||||
val page = pages.first { it.imageUrl?.contains(dto.imageUrl, true)!! }
|
||||
val fragment = json.encodeToString<List<Dialog>>(
|
||||
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"
|
||||
}
|
||||
}
|
@ -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<Dialog> = 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<String, String> = emptyMap(),
|
||||
val type: String = "normal",
|
||||
private val fbColor: List<Int> = emptyList(),
|
||||
private val bgColor: List<Int> = 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<List<Dialog>>(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
|
||||
}
|
@ -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),
|
||||
)
|
@ -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",
|
||||
)
|
@ -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<List<Dialog>>()
|
||||
?: 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:
|
||||
* <pre>{@code
|
||||
* val typeface: TypeFace? = loadFont("filename.ttf")
|
||||
* }</pre>
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}
|
@ -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<List<Dialog>>()
|
||||
?: 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,
|
||||
),
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
@ -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<List<TranslateDto>>()
|
||||
.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 <reified T> 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()
|
||||
}
|
||||
}
|
@ -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<TextTranslated>,
|
||||
) {
|
||||
val text = translations.firstOrNull()?.text ?: ""
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class TextTranslated(
|
||||
val text: String,
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String, Any?>,
|
||||
val response: Response,
|
||||
)
|
@ -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
|
||||
}
|