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 {
|
ext {
|
||||||
extName = 'Manhuarm'
|
extName = 'Manhuarm'
|
||||||
extClass = '.Manhuarm'
|
extClass = '.ManhuarmFactory'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://manhuarm.com'
|
baseUrl = 'https://manhuarm.com'
|
||||||
overrideVersionCode = 0
|
overrideVersionCode = 1
|
||||||
isNsfw = true
|
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
|
|
||||||
}
|
|