Remove MTL (#10408)
@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslationsUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data
|
|
||||||
android:host="${SOURCEHOST}"
|
|
||||||
android:pathPattern="/.*/..*"
|
|
||||||
android:scheme="${SOURCESCHEME}" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,25 +0,0 @@
|
|||||||
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
|
|
@ -1,93 +0,0 @@
|
|||||||
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.
|
|
@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
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.
|
|
@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
default_font_name_title=Web site
|
|
||||||
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
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Tamaño de letra
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Taille de la police
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Ukuran font
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Dimensione del carattere
|
|
@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
default_font_name_title=Site
|
|
||||||
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
|
|
@ -1,9 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("lib-multisrc")
|
|
||||||
}
|
|
||||||
|
|
||||||
baseVersionCode = 9
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(project(":lib:i18n"))
|
|
||||||
}
|
|
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 45 KiB |
@ -1,501 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
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.lib.i18n.Intl
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.TranslationInterceptor
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.TranslatorEngine
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.bing.BingTranslator
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.translator.google.GoogleTranslator
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
|
||||||
import keiyoushi.utils.parseAs
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
abstract class MachineTranslations(
|
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
private val language: Language,
|
|
||||||
) : ParsedHttpSource(), ConfigurableSource {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override val lang = language.lang
|
|
||||||
|
|
||||||
protected 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
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var fontSize: Int
|
|
||||||
get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt()
|
|
||||||
set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply()
|
|
||||||
|
|
||||||
protected var fontName: String
|
|
||||||
get() = preferences.getString(FONT_NAME_PREF, language.fontName)!!
|
|
||||||
set(value) = preferences.edit().putString(FONT_NAME_PREF, value).apply()
|
|
||||||
|
|
||||||
protected var disableWordBreak: Boolean
|
|
||||||
get() = preferences.getBoolean(DISABLE_WORD_BREAK_PREF, language.disableWordBreak)
|
|
||||||
set(value) = preferences.edit().putBoolean(DISABLE_WORD_BREAK_PREF, value).apply()
|
|
||||||
|
|
||||||
protected var disableTranslator: Boolean
|
|
||||||
get() = preferences.getBoolean(DISABLE_TRANSLATOR_PREF, language.disableTranslator)
|
|
||||||
set(value) = preferences.edit().putBoolean(DISABLE_TRANSLATOR_PREF, value).apply()
|
|
||||||
|
|
||||||
protected var enableMangaDetailsTranslation: Boolean
|
|
||||||
get() = preferences.getBoolean(ENABLE_MANGA_DETAILS_TRANSLATION_PREF, false)
|
|
||||||
set(value) = preferences.edit().putBoolean(ENABLE_MANGA_DETAILS_TRANSLATION_PREF, value).apply()
|
|
||||||
|
|
||||||
private val intl = Intl(
|
|
||||||
language = language.lang,
|
|
||||||
baseLanguage = "en",
|
|
||||||
availableLanguages = setOf("en", "es", "fr", "id", "it", "pt-BR"),
|
|
||||||
classLoader = this::class.java.classLoader!!,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val settings get() = language.copy(
|
|
||||||
fontSize = this@MachineTranslations.fontSize,
|
|
||||||
fontName = this@MachineTranslations.fontName,
|
|
||||||
disableWordBreak = this@MachineTranslations.disableWordBreak,
|
|
||||||
disableTranslator = this@MachineTranslations.disableTranslator,
|
|
||||||
disableFontSettings = this@MachineTranslations.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
|
|
||||||
|
|
||||||
protected 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(baseUrl, settings))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder {
|
|
||||||
return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
|
|
||||||
private val popularFilter = FilterList(SelectionList("", listOf(Option(value = "views", query = "sort_by"))))
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
|
|
||||||
private val latestFilter = FilterList(SelectionList("", listOf(Option(value = "recent", query = "sort_by"))))
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
// =========================== Search ============================
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("page", page.toString())
|
|
||||||
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
url.addQueryParameter("query", query)
|
|
||||||
}
|
|
||||||
|
|
||||||
filters.forEach { filter ->
|
|
||||||
when (filter) {
|
|
||||||
is SelectionList -> {
|
|
||||||
val selected = filter.selected()
|
|
||||||
if (selected.value.isBlank()) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
url.addQueryParameter(selected.query, selected.value)
|
|
||||||
}
|
|
||||||
is GenreList -> {
|
|
||||||
filter.state.filter(GenreCheckBox::state).forEach { genre ->
|
|
||||||
url.addQueryParameter("genres", genre.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith(PREFIX_SEARCH)) {
|
|
||||||
val slug = query.removePrefix(PREFIX_SEARCH)
|
|
||||||
return fetchMangaDetails(SManga.create().apply { url = "/comics/$slug" }).map { manga ->
|
|
||||||
MangasPage(listOf(manga), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = "section h2 + div > div"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
title = element.selectFirst("h3")!!.text()
|
|
||||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
|
||||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector() = "a[href*=search]:contains(Next)"
|
|
||||||
|
|
||||||
// =========================== Manga Details ============================
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
title = document.selectFirst("h1")!!.text().i18n()
|
|
||||||
description = document.selectFirst("p:has(span:contains(Synopsis))")?.ownText()?.i18n()
|
|
||||||
author = document.selectFirst("p:has(span:contains(Author))")?.ownText()
|
|
||||||
genre = document.select("h2:contains(Genres) + div span").joinToString { it.text() }.i18n()
|
|
||||||
thumbnail_url = document.selectFirst("img.object-cover")?.absUrl("src")
|
|
||||||
document.selectFirst("p:has(span:contains(Status))")?.ownText()?.let {
|
|
||||||
status = when (it.lowercase()) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"complete" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setUrlWithoutDomain(document.location())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Chapters ==============================
|
|
||||||
override fun chapterListSelector() = "section li"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
element.selectFirst("a")!!.let {
|
|
||||||
name = it.ownText()
|
|
||||||
setUrlWithoutDomain(it.absUrl("href"))
|
|
||||||
}
|
|
||||||
date_upload = parseChapterDate(element.selectFirst("span")?.text())
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Pages ================================
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
val pages = document.selectFirst("div#json-data")
|
|
||||||
?.ownText()?.parseAs<List<PageDto>>()
|
|
||||||
?: throw Exception("Pages not found")
|
|
||||||
|
|
||||||
return pages.mapIndexed { index, dto ->
|
|
||||||
val imageUrl = when {
|
|
||||||
dto.imageUrl.startsWith("http") -> dto.imageUrl
|
|
||||||
else -> "https://${dto.imageUrl}"
|
|
||||||
}
|
|
||||||
val fragment = json.encodeToString<List<Dialog>>(
|
|
||||||
dto.dialogues.filter { it.getTextBy(language).isNotBlank() },
|
|
||||||
)
|
|
||||||
Page(index, imageUrl = "$imageUrl${fragment.toFragment()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = ""
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String?): Long {
|
|
||||||
date ?: return 0
|
|
||||||
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { parseRelativeDate(date) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseRelativeDate(date: String): Long {
|
|
||||||
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
|
||||||
val cal = Calendar.getInstance()
|
|
||||||
|
|
||||||
return when {
|
|
||||||
date.contains("day", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
|
||||||
date.contains("hour", true) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
|
||||||
date.contains("minute", true) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
|
||||||
date.contains("second", true) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
|
||||||
date.contains("week", true) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent bad fragments
|
|
||||||
fun String.toFragment(): String = "#${this.replace("#", "*")}"
|
|
||||||
|
|
||||||
private fun String.i18n(): String = when {
|
|
||||||
enableMangaDetailsTranslation -> translator.translate(language.origin, language.target, this)
|
|
||||||
else -> this
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Filters ================================
|
|
||||||
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
val filters = mutableListOf<Filter<*>>(
|
|
||||||
SelectionList("Sort", sortByList),
|
|
||||||
Filter.Separator(),
|
|
||||||
GenreList(title = "Genres", genres = genreList),
|
|
||||||
)
|
|
||||||
|
|
||||||
return FilterList(filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
intl["default_font_name_title"] to "",
|
|
||||||
intl["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 = intl["font_size_title"]
|
|
||||||
entries = sizes.map {
|
|
||||||
"${it}pt" + if (it == DEFAULT_FONT_SIZE) " - ${intl["default_font_size"]}" else ""
|
|
||||||
}.toTypedArray()
|
|
||||||
entryValues = sizes
|
|
||||||
|
|
||||||
summary = buildString {
|
|
||||||
appendLine(intl["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,
|
|
||||||
intl["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 = intl["font_name_title"]
|
|
||||||
entries = fonts.map {
|
|
||||||
it.first + if (it.second.isBlank()) " - ${intl["default_font_name"]}" else ""
|
|
||||||
}.toTypedArray()
|
|
||||||
entryValues = fonts.map { it.second }.toTypedArray()
|
|
||||||
summary = buildString {
|
|
||||||
appendLine(intl["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,
|
|
||||||
intl["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 = "⚠ ${intl["disable_word_break_title"]}"
|
|
||||||
summary = intl["disable_word_break_summary"]
|
|
||||||
setDefaultValue(language.disableWordBreak)
|
|
||||||
setOnPreferenceChange { _, newValue ->
|
|
||||||
disableWordBreak = newValue as Boolean
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
if (language.target == language.origin) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = ENABLE_MANGA_DETAILS_TRANSLATION_PREF
|
|
||||||
title = "⚠ ${intl["enable_manga_details_translation_title"]}"
|
|
||||||
summary = intl["enable_manga_details_translation_summary"]
|
|
||||||
setDefaultValue(enableMangaDetailsTranslation)
|
|
||||||
setOnPreferenceChange { _, newValue ->
|
|
||||||
enableMangaDetailsTranslation = newValue as Boolean
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
if (language.supportNativeTranslation) {
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = DISABLE_TRANSLATOR_PREF
|
|
||||||
title = "⚠ ${intl["disable_translator_title"]}"
|
|
||||||
summary = intl["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 = intl["translate_dialog_box_title"]
|
|
||||||
entries = translators
|
|
||||||
entryValues = translators
|
|
||||||
summary = buildString {
|
|
||||||
appendLine(intl["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,
|
|
||||||
"${intl["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.
|
|
||||||
*/
|
|
||||||
protected fun Preference.setOnPreferenceChange(onPreferenceChangeListener: Preference.OnPreferenceChangeListener) {
|
|
||||||
setOnPreferenceChangeListener { preference, newValue ->
|
|
||||||
isSettingsChanged = true
|
|
||||||
onPreferenceChangeListener.onPreferenceChange(preference, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
|
|
||||||
const val DEVICE_FONT = "device:"
|
|
||||||
const val PREFIX_SEARCH = "id:"
|
|
||||||
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 ENABLE_MANGA_DETAILS_TRANSLATION_PREF = "enableMangaDetailsTranslationPref"
|
|
||||||
private const val TRANSLATOR_PROVIDER_PREF = "translatorProviderPref"
|
|
||||||
private const val DEFAULT_FONT_SIZE = "24"
|
|
||||||
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
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("img_url")
|
|
||||||
val imageUrl: String,
|
|
||||||
|
|
||||||
@SerialName("translations")
|
|
||||||
@Serializable(with = DialogListSerializer::class)
|
|
||||||
val dialogues: List<Dialog> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
data class Dialog(
|
|
||||||
val x1: Float,
|
|
||||||
val y1: Float,
|
|
||||||
val x2: Float,
|
|
||||||
val y2: 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 width get() = x2 - x1
|
|
||||||
val height get() = y2 - y1
|
|
||||||
val centerY get() = (y2 + y1) / 2f
|
|
||||||
val centerX get() = (x2 + x1) / 2f
|
|
||||||
|
|
||||||
val foregroundColor: Int get() {
|
|
||||||
val color = fbColor.takeIf { it.isNotEmpty() }
|
|
||||||
?: return Color.BLACK
|
|
||||||
return Color.rgb(color[0], color[1], color[2])
|
|
||||||
}
|
|
||||||
|
|
||||||
val backgroundColor: Int get() {
|
|
||||||
val color = bgColor.takeIf { it.isNotEmpty() }
|
|
||||||
?: return Color.WHITE
|
|
||||||
return Color.rgb(color[0], color[1], color[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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("x1", coordinates[0])
|
|
||||||
put("y1", coordinates[1])
|
|
||||||
put("x2", coordinates[2])
|
|
||||||
put("y2", coordinates[3])
|
|
||||||
put("textByLanguage", textByLanguage)
|
|
||||||
|
|
||||||
if (jsonElement.isArray) {
|
|
||||||
return@buildJsonObject
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonElement.jsonObject.let { obj ->
|
|
||||||
obj["fg_color"]?.let { put("fbColor", it) }
|
|
||||||
obj["bg_color"]?.let { put("bgColor", it) }
|
|
||||||
obj["angle"]?.let { put("angle", it) }
|
|
||||||
obj["type"]?.let { put("type", it) }
|
|
||||||
obj["is_bold"]?.let { put("isBold", it) }
|
|
||||||
put("isNewApi", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCoordinates(element: JsonElement): JsonArray {
|
|
||||||
return when (element) {
|
|
||||||
is JsonArray -> element.jsonArray[0].jsonArray
|
|
||||||
else -> element.jsonObject["bbox"]?.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
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
class MachineTranslationsFactoryUtils
|
|
||||||
|
|
||||||
data class Language(
|
|
||||||
val lang: String,
|
|
||||||
val target: String = lang,
|
|
||||||
val origin: String = "en",
|
|
||||||
val fontSize: Int = 24,
|
|
||||||
val disableFontSettings: Boolean = false,
|
|
||||||
val disableWordBreak: Boolean = false,
|
|
||||||
val disableTranslator: Boolean = false,
|
|
||||||
val supportNativeTranslation: Boolean = false,
|
|
||||||
val fontName: String = "",
|
|
||||||
)
|
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
|
|
||||||
class SelectionList(displayName: String, private val vals: List<Option>, state: Int = 0) :
|
|
||||||
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
|
|
||||||
fun selected() = vals[state]
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Option(val name: String = "", val value: String = "", val query: String = "")
|
|
||||||
|
|
||||||
class GenreList(title: String, genres: List<Genre>) :
|
|
||||||
Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })
|
|
||||||
|
|
||||||
class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
|
|
||||||
|
|
||||||
class Genre(val name: String, val id: String = name)
|
|
||||||
|
|
||||||
val genreList: List<Genre> = listOf(
|
|
||||||
Genre("Action"),
|
|
||||||
Genre("Adult"),
|
|
||||||
Genre("Adventure"),
|
|
||||||
Genre("Comedy"),
|
|
||||||
Genre("Drama"),
|
|
||||||
Genre("Ecchi"),
|
|
||||||
Genre("Fantasy"),
|
|
||||||
Genre("Gender Bender"),
|
|
||||||
Genre("Harem"),
|
|
||||||
Genre("Historical"),
|
|
||||||
Genre("Horror"),
|
|
||||||
Genre("Josei"),
|
|
||||||
Genre("Lolicon"),
|
|
||||||
Genre("Martial Arts"),
|
|
||||||
Genre("Mature"),
|
|
||||||
Genre("Mecha"),
|
|
||||||
Genre("Mystery"),
|
|
||||||
Genre("Psychological"),
|
|
||||||
Genre("Romance"),
|
|
||||||
Genre("School Life"),
|
|
||||||
Genre("Sci-fi"),
|
|
||||||
Genre("Seinen"),
|
|
||||||
Genre("Shoujo"),
|
|
||||||
Genre("Shoujo Ai"),
|
|
||||||
Genre("Shounen"),
|
|
||||||
Genre("Shounen Ai"),
|
|
||||||
Genre("Slice of Life"),
|
|
||||||
Genre("Smut"),
|
|
||||||
Genre("Sports"),
|
|
||||||
Genre("Supernatural"),
|
|
||||||
Genre("Tragedy"),
|
|
||||||
Genre("Yaoi"),
|
|
||||||
Genre("Yuri"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val sortByList = listOf(
|
|
||||||
Option("All"),
|
|
||||||
Option("Most Views", "views"),
|
|
||||||
Option("Most Recent", "recent"),
|
|
||||||
).map { it.copy(query = "sort_by") }
|
|
@ -1,39 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class MachineTranslationsUrlActivity : Activity() {
|
|
||||||
|
|
||||||
private val tag = javaClass.simpleName
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
|
||||||
val item = pathSegments[1]
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
|
||||||
putExtra("query", "${MachineTranslations.PREFIX_SEARCH}$item")
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(mainIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e(tag, e.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e(tag, "could not parse uri from intent $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,288 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.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.multisrc.machinetranslations.Dialog
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations.Companion.PAGE_REGEX
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import keiyoushi.utils.parseAs
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
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(
|
|
||||||
baseUrl: String,
|
|
||||||
val language: Language,
|
|
||||||
) : Interceptor {
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val fontFamily: MutableMap<String, Pair<String, Typeface?>> = mutableMapOf(
|
|
||||||
"sub" to Pair<String, Typeface?>("$baseUrl/images/sub.ttf", null),
|
|
||||||
"sfx" to Pair<String, Typeface?>("$baseUrl/images/sfx.ttf", null),
|
|
||||||
"normal" to Pair<String, Typeface?>("$baseUrl/images/normal.ttf", null),
|
|
||||||
)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
// Load the fonts before opening the connection to load the image,
|
|
||||||
// so there aren't two open connections inside the interceptor.
|
|
||||||
if (!language.disableFontSettings) {
|
|
||||||
loadAllFont(chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(dialog.type))
|
|
||||||
val dialogBox = createDialogBox(dialog, textPaint)
|
|
||||||
val y = getYAxis(textPaint, dialog, dialogBox)
|
|
||||||
canvas.draw(textPaint, dialogBox, dialog, dialog.x1, 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(type: String): Typeface? {
|
|
||||||
if (language.disableFontSettings) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type in fontFamily) {
|
|
||||||
return fontFamily[type]?.second
|
|
||||||
}
|
|
||||||
|
|
||||||
return when (type) {
|
|
||||||
"inside", "outside" -> fontFamily["sfx"]?.second
|
|
||||||
else -> fontFamily["normal"]?.second
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadAllFont(chain: Interceptor.Chain) {
|
|
||||||
val fallback = loadFont("coming_soon_regular.ttf")
|
|
||||||
fontFamily.keys.forEach { key ->
|
|
||||||
val font = fontFamily[key] ?: return@forEach
|
|
||||||
if (font.second != null) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
if (language.fontName.isNotBlank()) {
|
|
||||||
fontFamily[key] = key to loadFont("${language.fontName}.ttf")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
fontFamily[key] = key to (loadRemoteFont(font.first, chain) ?: fallback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads a remote font and converts it into a usable font object.
|
|
||||||
*
|
|
||||||
* This function makes an HTTP request to download a font from a specified remote URL.
|
|
||||||
* It then converts the response into a usable font object.
|
|
||||||
*/
|
|
||||||
private fun loadRemoteFont(fontUrl: String, chain: Interceptor.Chain): Typeface? {
|
|
||||||
return try {
|
|
||||||
val request = GET(fontUrl, chain.request().headers)
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
|
|
||||||
if (response.isSuccessful.not()) {
|
|
||||||
response.close()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val fontName = request.url.pathSegments.last()
|
|
||||||
response.body.use {
|
|
||||||
it.byteStream().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.y1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Dialog
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations.Companion.PAGE_REGEX
|
|
||||||
import 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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.translator
|
|
||||||
|
|
||||||
interface TranslatorEngine {
|
|
||||||
val capacity: Int
|
|
||||||
fun translate(from: String, to: String, text: String): String
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.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,
|
|
||||||
)
|
|
@ -1,84 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations.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,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".en.snowmtl.SnowmtlUrlActivity"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="snowmtl.ru"
|
|
||||||
android:pathPattern="/comics/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,10 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Snow Machine Translations'
|
|
||||||
extClass = '.SnowmtlFactory'
|
|
||||||
themePkg = 'machinetranslations'
|
|
||||||
baseUrl = 'https://snowmtl.ru'
|
|
||||||
overrideVersionCode = 10
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 39 KiB |
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.snowmtl
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class Snowmtl(
|
|
||||||
language: Language,
|
|
||||||
) : MachineTranslations(
|
|
||||||
name = "Snow Machine Translations",
|
|
||||||
baseUrl = "https://snowmtl.ru",
|
|
||||||
language,
|
|
||||||
)
|
|
@ -1,21 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.snowmtl
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class SnowmtlFactory : SourceFactory {
|
|
||||||
override fun createSources() = languageList.map(::Snowmtl)
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|
@ -1,10 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Solar Machine Translations'
|
|
||||||
extClass = '.SolarmtlFactory'
|
|
||||||
themePkg = 'machinetranslations'
|
|
||||||
baseUrl = 'https://solarmtl.com'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
@ -1,15 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.solarmtl
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.MachineTranslations
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class Solarmtl(
|
|
||||||
language: Language,
|
|
||||||
) : MachineTranslations(
|
|
||||||
name = "Solar Machine Translations",
|
|
||||||
baseUrl = "https://solarmtl.com",
|
|
||||||
language,
|
|
||||||
)
|
|
@ -1,21 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.solarmtl
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.Language
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
class SolarmtlFactory : SourceFactory {
|
|
||||||
override fun createSources() = languageList.map(::Solarmtl)
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
)
|
|