Compare commits
120 Commits
152405613e
...
56f64ce41c
Author | SHA1 | Date |
---|---|---|
beerpsi | 56f64ce41c | |
stevenyomi | 280d7eb814 | |
beerpsi | dccbfba8a6 | |
stevenyomi | a548a4fb4a | |
stevenyomi | 4f1e8b8c4f | |
AwkwardPeak7 | 9f21ec0d06 | |
beerpsi | 46c51293c7 | |
AwkwardPeak7 | c1c0886c0d | |
Claudemirovsky | 3f2882c523 | |
Claudemirovsky | 73be84037b | |
Mike | 95fc651011 | |
Mike | 2805da863a | |
stevenyomi | 9db6152418 | |
stevenyomi | 12e864f85f | |
beerpiss | 52c1b103cb | |
beerpsi | 512fb0053a | |
beerpsi | 7cc72c3fe7 | |
beerpsi | c5e7cad6a8 | |
beerpsi | acad777e6b | |
Mike | 8e712cc062 | |
stevenyomi | e3638b172f | |
Mike | c587983c99 | |
Fermín Cirella | 00f2eddb58 | |
beerpsi | bcfc74ef31 | |
beerpsi | cabade3e41 | |
beerpsi | 45c6f6a2b9 | |
beerpsi | 8504999a5c | |
AwkwardPeak7 | 96775f8190 | |
beerpsi | 43c5cd4943 | |
beerpsi | 37866f48f2 | |
AwkwardPeak7 | 3b998e9766 | |
beerpsi | 28b71a2648 | |
beerpsi | 0d1497285f | |
stevenyomi | 2a287d7398 | |
stevenyomi | 308d68dce1 | |
Mike | 9ca3646702 | |
stevenyomi | cf9f358af1 | |
beerpsi | d70973dbf7 | |
felixfon | 72283a476a | |
beerpsi | eb364ada84 | |
Chopper | 1cf4e97133 | |
beerpsi | 94e268186f | |
beerpsi | 4eb24b350b | |
beerpsi | 259e12ad25 | |
Mike | 34bc7e7a7a | |
Mike | 8264837faa | |
Mike | 57a90845b2 | |
Mike | 8b007abea8 | |
Mike | 4ebbed3da5 | |
Mike | 5f58182844 | |
Mike | 21991147c7 | |
beerpsi | a1d4f70db4 | |
Claudemirovsky | 5325cad42d | |
Secozzi | dbeab15596 | |
Mike | 4d211ad7c1 | |
Mike | f868bc5562 | |
beerpsi | 8933c61115 | |
AwkwardPeak7 | 6acac9416c | |
beerpsi | 0a0251c9d7 | |
bapeey | fdc8131482 | |
bapeey | 7f8350e669 | |
beerpsi | 01e27823f6 | |
beerpsi | d63bd90ef9 | |
AwkwardPeak7 | 5da654c4fc | |
Secozzi | 235f279d4b | |
beerpsi | 2da54739eb | |
beerpsi | 5d22f256b3 | |
beerpsi | a6dc08eb88 | |
Claudemirovsky | 4254b88c40 | |
stevenyomi | 01c097b7e6 | |
Mike | 968d1cb0ac | |
Mike | 6432a9abd7 | |
Luqman | 583197e12c | |
Luqman | a75f8b2428 | |
Mike | 619bcf6002 | |
Mike | 3fe2a0e70c | |
Mike | 401c4672e2 | |
Mike | 10fda994f0 | |
Mike | 3361fe2437 | |
beerpsi | 1d563cd3ca | |
NotBlankyu | 8938b92e09 | |
Claudemirovsky | 85af4c5f97 | |
Secozzi | 96c6804ede | |
Johannes Jöns | e3362fd497 | |
Johannes Jöns | ae32103858 | |
Deivid Gabriel Pereira de Oliveira | 1c34c9e56f | |
Secozzi | a3e5bcf32c | |
Luqman | 5c12784375 | |
Luqman | 04f5090aec | |
Luqman | 9db2ef2f59 | |
Luqman | 912f197ab6 | |
Claudemirovsky | fa359b535b | |
Luqman | d55b796c50 | |
Chopper | 2860c07078 | |
Claudemirovsky | a1090d63d7 | |
Claudemirovsky | bb7d02c94f | |
beerpsi | 56c2069e05 | |
beerpsi | 8ba3b24363 | |
beerpsi | 75387c7d98 | |
beerpsi | 521808940f | |
beerpsi | f2658d0619 | |
beerpsi | 608ed393bd | |
beerpsi | e9fe2131e7 | |
beerpsi | 49c8180aa1 | |
beerpiss | 61f58db749 | |
Deivid Gabriel Pereira de Oliveira | 815d8ba8d9 | |
Claudemirovsky | f505654fe7 | |
beerpsi | 5710e5634e | |
beerpsi | d976177365 | |
happywillow0 | 7ae0c27e21 | |
Mike | d631818f99 | |
Secozzi | 179f70e77c | |
Mike | fa3fc7188f | |
anenasa | a4c0420bf7 | |
Secozzi | f3b39d57ef | |
Mike | 800c9d416e | |
beerpsi | 0bb60c35a6 | |
Luqman | 405bff2301 | |
AwkwardPeak7 | 863c51dec6 | |
Luqman | 9759754b13 |
|
@ -0,0 +1,2 @@
|
|||
/gradlew linguist-generated
|
||||
/gradlew.bat linguist-generated
|
|
@ -198,7 +198,7 @@ The simplest extension structure looks like this:
|
|||
```console
|
||||
$ tree src/<lang>/<mysourcename>/
|
||||
src/<lang>/<mysourcename>/
|
||||
├── AndroidManifest.xml
|
||||
├── AndroidManifest.xml (optional)
|
||||
├── build.gradle
|
||||
├── res
|
||||
│ ├── mipmap-hdpi
|
||||
|
@ -227,15 +227,14 @@ src/<lang>/<mysourcename>/
|
|||
should be adapted from the site name, and can only contain lowercase ASCII letters and digits.
|
||||
Your extension code must be placed in the package `eu.kanade.tachiyomi.extension.<lang>.<mysourcename>`.
|
||||
|
||||
#### AndroidManifest.xml
|
||||
A minimal [Android manifest file](https://developer.android.com/guide/topics/manifest/manifest-intro)
|
||||
is needed for Android to recognize an extension when it's compiled into an APK file. You can also add
|
||||
intent filters inside this file (see [URL intent filter](#url-intent-filter) for more information).
|
||||
#### AndroidManifest.xml (optional)
|
||||
You only need to create this file if you want to add deep linking to your extension.
|
||||
See [URL intent filter](#url-intent-filter) for more information.
|
||||
|
||||
#### build.gradle
|
||||
Make sure that your new extension's `build.gradle` file follows the following structure:
|
||||
|
||||
```gradle
|
||||
```groovy
|
||||
ext {
|
||||
extName = '<My source name>'
|
||||
extClass = '.<MySourceName>'
|
||||
|
@ -251,10 +250,9 @@ apply from: "$rootDir/common.gradle"
|
|||
| `extName` | The name of the extension. Should be romanized if site name is not in English. |
|
||||
| `extClass` | Points to the class that implements `Source`. You can use a relative path starting with a dot (the package name is the base path). This is used to find and instantiate the source(s). |
|
||||
| `extVersionCode` | The extension version code. This must be a positive integer and incremented with any change to the code. |
|
||||
| `libVersion` | (Optional, defaults to `1.4`) The version of the [extensions library](https://github.com/tachiyomiorg/extensions-lib) used. |
|
||||
| `isNsfw` | (Optional, defaults to `false`) Flag to indicate that a source contains NSFW content. |
|
||||
|
||||
The extension's version name is generated automatically by concatenating `libVersion` and `extVersionCode`.
|
||||
The extension's version name is generated automatically by concatenating `1.4` and `extVersionCode`.
|
||||
With the example used above, the version would be `1.4.1`.
|
||||
|
||||
### Core dependencies
|
||||
|
@ -272,7 +270,7 @@ Referencing the actual implementation will help with understanding extensions' c
|
|||
for handling [base 64 encoded image data](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)
|
||||
using an [OkHttp interceptor](https://square.github.io/okhttp/interceptors/).
|
||||
|
||||
```gradle
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation(project(':lib-dataimage'))
|
||||
}
|
||||
|
@ -284,7 +282,7 @@ dependencies {
|
|||
internationalization in the sources. It allows loading `.properties` files with messages located under
|
||||
the `assets/i18n` folder of each extension, that can be used to translate strings under the source.
|
||||
|
||||
```gradle
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation(project(':lib-i18n'))
|
||||
}
|
||||
|
|
|
@ -19,7 +19,3 @@ allprojects {
|
|||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory.asFile.get())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
|
@ -1,6 +1,5 @@
|
|||
object AndroidConfig {
|
||||
const val compileSdk = 34
|
||||
const val minSdk = 21
|
||||
@Suppress("UNUSED")
|
||||
const val targetSdk = 34
|
||||
}
|
||||
|
|
|
@ -3,8 +3,11 @@ apply plugin: 'kotlin-android'
|
|||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'org.jmailen.kotlinter'
|
||||
|
||||
assert !ext.has("pkgNameSuffix")
|
||||
assert !ext.has("libVersion")
|
||||
|
||||
android {
|
||||
compileSdkVersion AndroidConfig.compileSdk
|
||||
compileSdk AndroidConfig.compileSdk
|
||||
|
||||
namespace "eu.kanade.tachiyomi.extension"
|
||||
sourceSets {
|
||||
|
@ -14,39 +17,22 @@ android {
|
|||
res.srcDirs = ['res']
|
||||
assets.srcDirs = ['assets']
|
||||
}
|
||||
release {
|
||||
manifest.srcFile "AndroidManifest.xml"
|
||||
}
|
||||
debug {
|
||||
manifest.srcFile "AndroidManifest.xml"
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion AndroidConfig.minSdk
|
||||
targetSdkVersion AndroidConfig.targetSdk
|
||||
minSdk AndroidConfig.minSdk
|
||||
targetSdk AndroidConfig.targetSdk
|
||||
applicationIdSuffix project.parent.name + "." + project.name
|
||||
versionCode extVersionCode
|
||||
versionName project.ext.properties.getOrDefault("libVersion", "1.4") + ".$extVersionCode"
|
||||
versionName "1.4.$versionCode"
|
||||
base {
|
||||
archivesName = "tachiyomi-$applicationIdSuffix-v$versionName"
|
||||
}
|
||||
def readmes = project.projectDir.listFiles({ File file ->
|
||||
file.name == "README.md" || file.name == "CHANGELOG.md"
|
||||
} as FileFilter)
|
||||
def hasReadme = readmes != null && readmes.any { File file ->
|
||||
file.name.startsWith("README")
|
||||
}
|
||||
def hasChangelog = readmes != null && readmes.any { File file ->
|
||||
file.name.startsWith("CHANGELOG")
|
||||
}
|
||||
assert extClass.startsWith(".")
|
||||
manifestPlaceholders = [
|
||||
appName : "Tachiyomi: $extName",
|
||||
extClass: extClass,
|
||||
extFactory: project.ext.properties.getOrDefault("extFactory", ""),
|
||||
nsfw: project.ext.properties.getOrDefault("isNsfw", false) ? 1 : 0,
|
||||
hasReadme: hasReadme ? 1 : 0,
|
||||
hasChangelog: hasChangelog ? 1 : 0,
|
||||
nsfw: project.ext.find("isNsfw") ? 1 : 0,
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -71,9 +57,6 @@ android {
|
|||
}
|
||||
|
||||
buildFeatures {
|
||||
// Disable unused AGP features
|
||||
aidl false
|
||||
renderScript false
|
||||
resValues false
|
||||
shaders false
|
||||
buildConfig true
|
||||
|
@ -107,5 +90,20 @@ dependencies {
|
|||
compileOnly(libs.bundles.common)
|
||||
}
|
||||
|
||||
preBuild.dependsOn(lintKotlin)
|
||||
tasks.register("writeManifestFile") {
|
||||
doLast {
|
||||
def manifest = android.sourceSets.getByName("main").manifest
|
||||
if (!manifest.srcFile.exists()) {
|
||||
File tempFile = layout.buildDirectory.get().file("tempAndroidManifest.xml").getAsFile()
|
||||
if (!tempFile.exists()) {
|
||||
tempFile.withWriter {
|
||||
it.write('<?xml version="1.0" encoding="utf-8"?>\n<manifest />\n')
|
||||
}
|
||||
}
|
||||
manifest.srcFile(tempFile.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preBuild.dependsOn(writeManifestFile, lintKotlin)
|
||||
lintKotlin.dependsOn(formatKotlin)
|
||||
|
|
|
@ -6,10 +6,7 @@
|
|||
<application android:icon="@mipmap/ic_launcher" android:allowBackup="false" android:label="${appName}">
|
||||
|
||||
<meta-data android:name="tachiyomi.extension.class" android:value="${extClass}" />
|
||||
<meta-data android:name="tachiyomi.extension.factory" android:value="${extFactory}" />
|
||||
<meta-data android:name="tachiyomi.extension.nsfw" android:value="${nsfw}" />
|
||||
<meta-data android:name="tachiyomi.extension.hasReadme" android:value="${hasReadme}" />
|
||||
<meta-data android:name="tachiyomi.extension.hasChangelog" android:value="${hasChangelog}" />
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ android {
|
|||
|
||||
namespace = "eu.kanade.tachiyomi.extension.core"
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
sourceSets {
|
||||
named("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
|
@ -19,9 +18,8 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
libraryVariants.all {
|
||||
generateBuildConfigProvider?.configure {
|
||||
enabled = false
|
||||
}
|
||||
buildFeatures {
|
||||
resValues = false
|
||||
shaders = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,10 +83,8 @@ done
|
|||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -133,10 +131,13 @@ location of your Java installation."
|
|||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
|
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
|
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
@ -197,6 +198,10 @@ if "$cygwin" || "$msys" ; then
|
|||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
|
|
|
@ -39,4 +39,4 @@ object Deobfuscator {
|
|||
}
|
||||
|
||||
// Update this when the script is updated!
|
||||
private const val SCRIPT_NAME = "synchrony-v2.4.2.1.js"
|
||||
private const val SCRIPT_NAME = "synchrony-v2.4.5.1.js"
|
||||
|
|
|
@ -31,6 +31,10 @@ configurations {
|
|||
dependencies {
|
||||
compileOnly(libs.bundles.common)
|
||||
|
||||
// Only PeachScan sources uses the image-decoder dependency.
|
||||
//noinspection UseTomlInstead
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290")
|
||||
|
||||
// Implements all :lib libraries on the multisrc generator
|
||||
// Note that this does not mean that generated sources are going to
|
||||
// implement them too; this is just to be able to compile and generate sources.
|
||||
|
|
|
@ -27,9 +27,15 @@ class BilibiliManga : Bilibili(
|
|||
return emptyList()
|
||||
}
|
||||
|
||||
return result.data!!.episodeList
|
||||
.filter { episode -> episode.isInFree || !episode.isLocked }
|
||||
.map { ep -> chapterFromObject(ep, result.data.id) }
|
||||
val data = result.data!!
|
||||
val id = data.id
|
||||
return data.episodeList.mapNotNull { episode ->
|
||||
if (episode.isInFree || !episode.isLocked) {
|
||||
chapterFromObject(episode, id)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val defaultPopularSort: Int = 0
|
||||
|
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,67 @@
|
|||
package eu.kanade.tachiyomi.extension.vi.blogtruyen
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.blogtruyen.BlogTruyen
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
|
||||
class BlogTruyenMoi : BlogTruyen("BlogTruyen", "https://blogtruyenmoi.com", "vi") {
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun getGenreList() = listOf(
|
||||
Genre("Action", "1"),
|
||||
Genre("Adventure", "3"),
|
||||
Genre("Comedy", "5"),
|
||||
Genre("Comic", "6"),
|
||||
Genre("Doujinshi", "7"),
|
||||
Genre("Drama", "49"),
|
||||
Genre("Ecchi", "48"),
|
||||
Genre("Event BT", "60"),
|
||||
Genre("Fantasy", "50"),
|
||||
Genre("Full màu", "64"),
|
||||
Genre("Game", "61"),
|
||||
Genre("Gender Bender", "51"),
|
||||
Genre("Harem", "12"),
|
||||
Genre("Historical", "13"),
|
||||
Genre("Horror", "14"),
|
||||
Genre("Isekai/Dị giới/Trọng sinh", "63"),
|
||||
Genre("Josei", "15"),
|
||||
Genre("Live action", "16"),
|
||||
Genre("Magic", "46"),
|
||||
Genre("manga", "55"),
|
||||
Genre("Manhua", "17"),
|
||||
Genre("Manhwa", "18"),
|
||||
Genre("Martial Arts", "19"),
|
||||
Genre("Mecha", "21"),
|
||||
Genre("Mystery", "22"),
|
||||
Genre("Nấu Ăn", "56"),
|
||||
Genre("Ngôn Tình", "65"),
|
||||
Genre("NTR", "62"),
|
||||
Genre("One shot", "23"),
|
||||
Genre("Psychological", "24"),
|
||||
Genre("Romance", "25"),
|
||||
Genre("School Life", "26"),
|
||||
Genre("Sci-fi", "27"),
|
||||
Genre("Seinen", "28"),
|
||||
Genre("Shoujo", "29"),
|
||||
Genre("Shoujo Ai", "30"),
|
||||
Genre("Shounen", "31"),
|
||||
Genre("Shounen Ai", "32"),
|
||||
Genre("Slice of life", "33"),
|
||||
Genre("Smut", "34"),
|
||||
Genre("Soft Yaoi", "35"),
|
||||
Genre("Soft Yuri", "36"),
|
||||
Genre("Sports", "37"),
|
||||
Genre("Supernatural", "38"),
|
||||
Genre("Tạp chí truyện tranh", "39"),
|
||||
Genre("Tragedy", "40"),
|
||||
Genre("Trap (Crossdressing)", "58"),
|
||||
Genre("Trinh Thám", "57"),
|
||||
Genre("Truyện scan", "41"),
|
||||
Genre("Tu chân - tu tiên", "66"),
|
||||
Genre("Video Clip", "53"),
|
||||
Genre("VnComic", "42"),
|
||||
Genre("Webtoon", "52"),
|
||||
Genre("Yuri", "59"),
|
||||
)
|
||||
}
|
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 8.2 KiB |
|
@ -0,0 +1,56 @@
|
|||
package eu.kanade.tachiyomi.extension.vi.blogtruyenvn
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.blogtruyen.BlogTruyen
|
||||
|
||||
class BlogTruyenVn : BlogTruyen("BlogTruyen.vn (unoriginal)", "https://blogtruyenvn.com", "vi") {
|
||||
override fun getGenreList() = listOf(
|
||||
Genre("Action", "1"),
|
||||
Genre("Adventure", "3"),
|
||||
Genre("Comedy", "5"),
|
||||
Genre("Comic", "6"),
|
||||
Genre("Doujinshi", "7"),
|
||||
Genre("Drama", "49"),
|
||||
Genre("Ecchi", "48"),
|
||||
Genre("Event BT", "60"),
|
||||
Genre("Fantasy", "50"),
|
||||
Genre("Full màu", "64"),
|
||||
Genre("Game", "61"),
|
||||
Genre("Harem", "12"),
|
||||
Genre("Historical", "13"),
|
||||
Genre("Horror", "14"),
|
||||
Genre("Isekai/Dị giới/Trọng sinh", "63"),
|
||||
Genre("Josei", "15"),
|
||||
Genre("Live action", "16"),
|
||||
Genre("Magic", "46"),
|
||||
Genre("manga", "55"),
|
||||
Genre("Manhua", "17"),
|
||||
Genre("Manhwa", "18"),
|
||||
Genre("Martial Arts", "19"),
|
||||
Genre("Mecha", "21"),
|
||||
Genre("Mystery", "22"),
|
||||
Genre("Nấu Ăn", "56"),
|
||||
Genre("Ngôn Tình", "65"),
|
||||
Genre("NTR", "62"),
|
||||
Genre("One shot", "23"),
|
||||
Genre("Psychological", "24"),
|
||||
Genre("Romance", "25"),
|
||||
Genre("School Life", "26"),
|
||||
Genre("Sci-fi", "27"),
|
||||
Genre("Seinen", "28"),
|
||||
Genre("Shoujo", "29"),
|
||||
Genre("Shounen", "31"),
|
||||
Genre("Shounen Ai", "32"),
|
||||
Genre("Slice of life", "33"),
|
||||
Genre("Smut", "34"),
|
||||
Genre("Sports", "37"),
|
||||
Genre("Supernatural", "38"),
|
||||
Genre("Tạp chí truyện tranh", "39"),
|
||||
Genre("Tragedy", "40"),
|
||||
Genre("Trinh Thám", "57"),
|
||||
Genre("Truyện scan", "41"),
|
||||
Genre("Tu chân - tu tiên", "66"),
|
||||
Genre("Video Clip", "53"),
|
||||
Genre("VnComic", "42"),
|
||||
Genre("Webtoon", "52"),
|
||||
)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity android:name=".vi.blogtruyen.BlogTruyenUrlActivity"
|
||||
<activity android:name="eu.kanade.tachiyomi.multisrc.blogtruyen.BlogTruyenUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
|
@ -11,9 +11,9 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="blogtruyenmoi.com" />
|
||||
<data android:host="m.blogtruyenmoi.com" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="${SOURCEHOST}" />
|
||||
<data android:host="m.${SOURCEHOST}" />
|
||||
<data android:scheme="${SOURCESCHEME}" />
|
||||
|
||||
<data android:pathPattern="/tac-gia/..*" />
|
||||
<data android:pathPattern="/nhom-dich/..*" />
|
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,56 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.tr.epikmanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
|
||||
class EpikManga : FMReader("Epik Manga", "https://www.epikmanga.com", "tr") {
|
||||
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/seri-listesi?sorting=views&sorting-type=DESC&Sayfa=$page", headers)
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/seri-listesi?sorting=lastUpdate&sorting-type=DESC&Sayfa=$page", headers)
|
||||
override fun popularMangaNextPageSelector() = "ul.pagination li.active + li:not(.disabled)"
|
||||
|
||||
override val headerSelector = "h4 a"
|
||||
|
||||
// search wasn't working on source's website
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, query)
|
||||
}
|
||||
}
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/seri-listesi?type=text", headers)
|
||||
private fun searchMangaParse(response: Response, query: String): MangasPage {
|
||||
val mangas = response.asJsoup().select("div.char.col-lg-4 a").toList()
|
||||
.filter { it.text().contains(query, ignoreCase = true) }
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text()
|
||||
}
|
||||
}
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.col-md-9 div.row").first()!!
|
||||
|
||||
return SManga.create().apply {
|
||||
status = parseStatus(infoElement.select("h4:contains(Durum:)").firstOrNull()?.ownText())
|
||||
author = infoElement.select("h4:contains(Yazar:)").firstOrNull()?.ownText()
|
||||
artist = infoElement.select("h4:contains(Çizer:)").firstOrNull()?.ownText()
|
||||
genre = infoElement.select("h4:contains(Türler:) a").joinToString { it.text() }
|
||||
thumbnail_url = infoElement.select("img.thumbnail").imgAttr()
|
||||
description = document.select("div.col-md-12 p").text()
|
||||
}
|
||||
}
|
||||
override fun chapterListSelector() = "table.table tbody tr"
|
||||
override fun getFilterList(): FilterList = FilterList()
|
||||
}
|
|
@ -1,21 +1,41 @@
|
|||
package eu.kanade.tachiyomi.extension.ja.manga1000
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import org.jsoup.nodes.Element
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import java.util.Calendar
|
||||
|
||||
class Manga1000 : FMReader("Manga1000", "https://manga1000.top", "ja") {
|
||||
override fun chapterFromElement(element: Element, mangaTitle: String): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.let {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
name = it.attr("title")
|
||||
}
|
||||
// source is picky about URL format
|
||||
private fun mangaRequest(sortBy: String, page: Int): Request {
|
||||
return GET("$baseUrl/manga-list.html?listType=pagination&page=$page&artist=&author=&group=&m_status=&name=&genre=&ungenre=&magazine=&sort=$sortBy&sort_type=DESC", headers)
|
||||
}
|
||||
|
||||
date_upload = element.select(chapterTimeSelector)
|
||||
.let { if (it.hasText()) parseChapterDate(it.text()) else 0 }
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request = mangaRequest("views", page)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = mangaRequest("last_update", page)
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val slug = manga.url.substringAfter("manga-").substringBefore(".html")
|
||||
|
||||
return client.newCall(GET("$baseUrl/app/manga/controllers/cont.Listchapterapi.php?slug=$slug", headers))
|
||||
.asObservableSuccess()
|
||||
.map { res ->
|
||||
res.asJsoup().select(".at-series a").map {
|
||||
SChapter.create().apply {
|
||||
name = it.select(".chapter-name").text()
|
||||
url = it.attr("abs:href").substringAfter("controllers")
|
||||
date_upload = parseChapterDate(it.select(".chapter-time").text())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
|
@ -37,4 +57,18 @@ class Manga1000 : FMReader("Manga1000", "https://manga1000.top", "ja") {
|
|||
|
||||
return chapterDate.timeInMillis
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("script:containsData(imgsListchap)")
|
||||
.html()
|
||||
.substringAfter("(")
|
||||
.substringBefore(",")
|
||||
.let { cid ->
|
||||
client.newCall(GET("$baseUrl/app/manga/controllers/cont.imgsList.php?cid=$cid", headers)).execute().asJsoup()
|
||||
}
|
||||
.select(".lazyload")
|
||||
.mapIndexed { i, e ->
|
||||
Page(i, "", e.attr("abs:data-src"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,82 @@
|
|||
package eu.kanade.tachiyomi.extension.ja.nicomanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class Nicomanga : FMReader("Nicomanga", "https://nicomanga.com", "ja") {
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
|
||||
override val infoElementSelector = ".card-body > div.row"
|
||||
override val mangaDetailsSelectorGenre = "li:has(b:contains(Genre)) a.btn-danger"
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val slug = urlRegex.find(manga.url)?.groupValues?.get(1) ?: throw Exception("Unable to get slug")
|
||||
val headers = headersBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
set("Referer", baseUrl + manga.url)
|
||||
}.build()
|
||||
return GET("$baseUrl/app/manga/controllers/cont.Listchapterapi.php?slug=$slug", headers)
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element, mangaTitle: String): SChapter = SChapter.create().apply {
|
||||
element.select(chapterUrlSelector).first()!!.let {
|
||||
setUrlWithoutDomain("$baseUrl/${it.attr("href")}")
|
||||
name = it.attr("title")
|
||||
}
|
||||
|
||||
date_upload = element.select(chapterTimeSelector)
|
||||
.let { if (it.hasText()) parseRelativeDate(it.text()) else 0 }
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val id = chapterIdRegex.find(response.use { it.body.string() })?.groupValues?.get(1) ?: throw Exception("chapter-id not found")
|
||||
val doc = client.newCall(
|
||||
GET("$baseUrl/app/manga/controllers/cont.imgsList.php?cid=$id", headers),
|
||||
).execute().asJsoup()
|
||||
return doc.select("img.chapter-img[data-src]").mapIndexed { i, page ->
|
||||
Page(i + 1, imageUrl = page.attr("data-src"))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
override fun getImgAttr(element: Element?): String? {
|
||||
return when {
|
||||
element?.attr("style")?.contains("background-image") == true -> {
|
||||
val url = thumbnailURLRegex.find(element.attr("style"))?.groupValues?.get(1)
|
||||
when {
|
||||
url?.startsWith("/") == true -> baseUrl + url
|
||||
else -> url
|
||||
}
|
||||
}
|
||||
else -> super.getImgAttr(element)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val thumbnailURLRegex = Regex("background-image:[^;]?url\\s*\\(\\s*'?([^')]+?)'?(\\)|\$)")
|
||||
private val urlRegex = Regex("manga-([^/]+)\\.html\$")
|
||||
private val chapterIdRegex = Regex("imgsListchap\\((\\d+)")
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 9.4 KiB |
|
@ -1,32 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.vi.saytruyen
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
|
||||
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.util.asJsoup
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
class SayTruyen : FMReader("Say Truyen", "https://saytruyenvip.com", "vi") {
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val info = document.select("div.row").first()!!
|
||||
return SManga.create().apply {
|
||||
author = info.select("div.row li:has(b:contains(Tác giả)) small").text()
|
||||
genre = info.select("div.row li:has(b:contains(Thể loại)) small a").joinToString { it.text() }
|
||||
status = parseStatus(info.select("div.row li:has(b:contains(Tình trạng)) a").text())
|
||||
description = document.select("div.description").text()
|
||||
thumbnail_url = info.select("img.thumbnail").attr("abs:src")
|
||||
}
|
||||
}
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.asJsoup().let { document ->
|
||||
document.select(chapterListSelector()).map {
|
||||
chapterFromElement(it).apply {
|
||||
scanlator = document.select("div.row li:has(b:contains(Nhóm dịch)) small").text()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun pageListParse(document: Document): List<Page> = super.pageListParse(document).onEach { it.imageUrl!!.trim() }
|
||||
}
|
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 13 KiB |
|
@ -1,27 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.baixarhentai
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.multisrc.foolslide.FoolSlide
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.jsoup.nodes.Document
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BaixarHentai : FoolSlide("Baixar Hentai", "https://leitura.baixarhentai.net", "pt-BR") {
|
||||
// Hardcode the id because the language wasn't specific.
|
||||
override val id = 8908032188831949972
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.rateLimit(1, 2, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
title = document.select("h1.title").text()
|
||||
thumbnail_url = getDetailsThumbnail(document, "div.title a")
|
||||
}
|
||||
|
||||
// Always show adult content
|
||||
override val allowAdult = true
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {}
|
||||
}
|
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB |
|
@ -1,15 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.fr.lecercleduscan
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.foolslide.FoolSlide
|
||||
import java.util.Locale
|
||||
|
||||
class LeCercleDuScan : FoolSlide("Le Cercle du Scan", "https://lel.lecercleduscan.com", "fr") {
|
||||
override fun parseChapterDate(date: String) = super.parseChapterDate(
|
||||
when (val lcDate = date.lowercase(Locale.FRENCH)) {
|
||||
"hier" -> "yesterday"
|
||||
"aujourd'hui" -> "today"
|
||||
"demain" -> "tomorrow"
|
||||
else -> lcDate
|
||||
},
|
||||
)
|
||||
}
|
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,10 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.hentaisphere
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hentaihand.HentaiHand
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class HentaiSphere : HentaiHand("HentaiSphere", "https://hentaisphere.com", "en", false) {
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
}
|
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 5.8 KiB |
|
@ -1,10 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.readmanhwa
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.hentaihand.HentaiHand
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class ReadManhwa : HentaiHand("ReadManhwa", "https://readmanhwa.com", "en", true) {
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { authIntercept(it) }
|
||||
.build()
|
||||
}
|
|
@ -1,25 +1,7 @@
|
|||
package eu.kanade.tachiyomi.extension.en.adultwebtoon
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Request
|
||||
|
||||
class AdultWebtoon : Madara("Adult Webtoon", "https://adultwebtoon.com", "en") {
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val pageSuffix = if (page != 1) "page/$page/" else ""
|
||||
return GET(
|
||||
"$baseUrl/manga/$pageSuffix?m_orderby=trending",
|
||||
headers,
|
||||
CacheControl.FORCE_NETWORK,
|
||||
)
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val pageSuffix = if (page != 1) "page/$page/" else ""
|
||||
return GET(
|
||||
"$baseUrl/manga/$pageSuffix?m_orderby=latest",
|
||||
headers,
|
||||
CacheControl.FORCE_NETWORK,
|
||||
)
|
||||
}
|
||||
override val mangaSubString = "adult-webtoon"
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.akumanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||
|
||||
class AkuManga : Madara("AkuManga", "https://akumanga.com", "en") {
|
||||
|
||||
override val id: Long = 107810123708352143
|
||||
|
||||
override val chapterUrlSuffix = ""
|
||||
}
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 1.7 KiB |