parent
99fce4c23e
commit
058d5905f0
15
src/en/arcrelight/build.gradle
Normal file
15
src/en/arcrelight/build.gradle
Normal file
@ -0,0 +1,15 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
appName = 'Tachiyomi: Arc-Relight'
|
||||
pkgNameSuffix = 'en.arcrelight'
|
||||
extClass = '.ArcRelight'
|
||||
extVersionCode = 1
|
||||
extVersionSuffix = 1
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
|
BIN
src/en/arcrelight/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/arcrelight/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
src/en/arcrelight/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/arcrelight/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
BIN
src/en/arcrelight/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/arcrelight/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
BIN
src/en/arcrelight/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/arcrelight/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
src/en/arcrelight/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/arcrelight/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
BIN
src/en/arcrelight/res/web_hi_res_512.png
Normal file
BIN
src/en/arcrelight/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@ -0,0 +1,64 @@
|
||||
package eu.kanade.tachiyomi.extension.en.arcrelight
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
/** Array containing the possible statuses of a manga */
|
||||
private val STATUSES = arrayOf("Any", "Completed", "Ongoing")
|
||||
|
||||
/** List containing the possible categories of a manga */
|
||||
private val CATEGORIES = listOf(
|
||||
Category("4-Koma"),
|
||||
Category("Chaos;Head"),
|
||||
Category("Comedy"),
|
||||
Category("Drama"),
|
||||
Category("Mystery"),
|
||||
Category("Psychological"),
|
||||
Category("Robotics;Notes"),
|
||||
Category("Romance"),
|
||||
Category("Sci-Fi"),
|
||||
Category("Seinen"),
|
||||
Category("Shounen"),
|
||||
Category("Steins;Gate"),
|
||||
Category("Supernatural"),
|
||||
Category("Tragedy")
|
||||
)
|
||||
|
||||
/**
|
||||
* Filter representing the status of a manga.
|
||||
*
|
||||
* @constructor Creates a [Filter.Select] object with [STATUSES].
|
||||
*/
|
||||
class Status : Filter.Select<String>("Status", STATUSES) {
|
||||
/** Returns the [state] as a string. */
|
||||
fun string() = values[state].toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter representing a manga category.
|
||||
*
|
||||
* @property name The display name of the category.
|
||||
* @constructor Creates a [Filter.TriState] object using [name].
|
||||
*/
|
||||
class Category(name: String) : Filter.TriState(name) {
|
||||
/** Returns the [state] as a string, or null if [isIgnored]. */
|
||||
fun stringOpt() = when(state) {
|
||||
STATE_INCLUDE -> name.toLowerCase()
|
||||
STATE_EXCLUDE -> "-" + name.toLowerCase()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter representing the [categories][Category] of a manga.
|
||||
*
|
||||
* @constructor Creates a [Filter.Group] object with [CATEGORIES].
|
||||
*/
|
||||
class CategoryList : Filter.Group<Category>("Categories", CATEGORIES)
|
||||
|
||||
/**
|
||||
* Filter representing the name of an author or artist.
|
||||
*
|
||||
* @constructor Creates a [Filter.Text] object.
|
||||
*/
|
||||
class Person : Filter.Text("Author/Artist")
|
||||
|
@ -0,0 +1,89 @@
|
||||
package eu.kanade.tachiyomi.extension.en.arcrelight
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.text.DecimalFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* The HTTP date format specified in
|
||||
* [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55).
|
||||
*/
|
||||
private const val HTTP_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"
|
||||
|
||||
/**
|
||||
* Converts a date in the [HTTP_DATE] format to a Unix timestamp.
|
||||
*
|
||||
* @param date The date to convert.
|
||||
* @return The timestamp of the date.
|
||||
*/
|
||||
fun httpDateToTimestamp(date: String) =
|
||||
SimpleDateFormat(HTTP_DATE, Locale.US).parse(date).time
|
||||
|
||||
/**
|
||||
* Joins each value of a given [field] of the array using [sep].
|
||||
*
|
||||
* @param field
|
||||
* When its type is [Int], it is treated as the index of a [JSONArray].
|
||||
* When its type is [String], it is treated as the key of a [JSONObject].
|
||||
* @param sep The separator used to join the array.
|
||||
* @return The joined string, or null if the array is empty.
|
||||
* @throws IllegalArgumentException when [field] is of an invalid type.
|
||||
*/
|
||||
fun JSONArray.joinField(field: Any, sep: String = ", "): String? {
|
||||
if (!(field is Int || field is String))
|
||||
throw IllegalArgumentException("field must be a String or Int")
|
||||
if (this.length() == 0) return null
|
||||
val list = mutableListOf<String>()
|
||||
for (i in 0 until this.length()) {
|
||||
when (field) {
|
||||
is Int -> list.add(this.getJSONArray(i).getString(field))
|
||||
is String -> list.add(this.getJSONObject(i).getString(field))
|
||||
}
|
||||
}
|
||||
return list.joinToString(sep)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [SManga] by parsing a [JSONObject].
|
||||
*
|
||||
* @param obj The object containing the manga info.
|
||||
*/
|
||||
fun SManga.fromJSON(obj: JSONObject) {
|
||||
url = obj.getString("url")
|
||||
title = obj.getString("title")
|
||||
description = obj.getString("description")
|
||||
thumbnail_url = obj.getString("cover")
|
||||
author = obj.getJSONArray("authors")?.joinField(0)
|
||||
artist = obj.getJSONArray("artists")?.joinField(0)
|
||||
genre = obj.getJSONArray("categories")?.joinField("name")
|
||||
status = when (obj.getBoolean("completed")) {
|
||||
true -> SManga.COMPLETED
|
||||
false -> SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [SChapter] by parsing a [JSONObject].
|
||||
*
|
||||
* @param obj The object containing the chapter info.
|
||||
*/
|
||||
fun SChapter.fromJSON(obj: JSONObject) {
|
||||
url = obj.getString("url")
|
||||
chapter_number = obj.getString("chapter").toFloat()
|
||||
date_upload = httpDateToTimestamp(obj.getString("date"))
|
||||
scanlator = obj.getJSONArray("groups")?.joinField("name", " & ")
|
||||
val vol = obj.getString("volume")
|
||||
val ch = DecimalFormat("0.#").format(chapter_number)
|
||||
name = buildString {
|
||||
if (vol != "0") append("Vol.$vol ")
|
||||
append("Ch.$ch - ")
|
||||
append(obj.getString("title"))
|
||||
if (obj.getBoolean("final")) append(" [END]")
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,149 @@
|
||||
package eu.kanade.tachiyomi.extension.en.arcrelight
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build.VERSION
|
||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
/** Arc-Relight source */
|
||||
class ArcRelight : HttpSource() {
|
||||
override val versionId = 1
|
||||
|
||||
override val name = "Arc-Relight"
|
||||
|
||||
override val baseUrl = "https://arc-relight.site/api/v$versionId"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
/**
|
||||
* A user agent representing Tachiyomi.
|
||||
* Includes the user's Android version
|
||||
* and the current extension version.
|
||||
*/
|
||||
private val userAgent = "Mozilla/5.0 (" +
|
||||
"Android ${VERSION.RELEASE}; Mobile) " +
|
||||
"Tachiyomi/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", userAgent)
|
||||
add("Referer", baseUrl)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/releases/", headers)
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET(Uri.parse(chapter.url).path.replace(
|
||||
"/reader/", "$baseUrl/series/"), headers)
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET("$baseUrl/series/${manga.url.split("/")
|
||||
.last { it != "" }}/", headers)
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
mangaDetailsRequest(manga)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String,
|
||||
filters: FilterList): Request {
|
||||
val uri = Uri.parse("$baseUrl/series/").buildUpon()
|
||||
uri.appendQueryParameter("q", query)
|
||||
val cat = mutableListOf<String>()
|
||||
filters.forEach {
|
||||
when (it) {
|
||||
is Person -> uri.appendQueryParameter("author", it.state)
|
||||
is Status -> uri.appendQueryParameter("status", it.string())
|
||||
is CategoryList -> cat.addAll(it.state.mapNotNull {
|
||||
c -> Uri.encode(c.stringOpt())
|
||||
})
|
||||
}
|
||||
}
|
||||
return GET("$uri&categories=${cat.joinToString(",")}", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val arr = JSONArray(response.body()!!.string())
|
||||
val ret = ArrayList<SManga>(arr.length())
|
||||
for (i in 0 until arr.length()) {
|
||||
val obj = arr.getJSONObject(i)
|
||||
ret.add(SManga.create().apply {
|
||||
url = obj.getString("url")
|
||||
title = obj.getString("title")
|
||||
thumbnail_url = obj.getString("cover")
|
||||
})
|
||||
}
|
||||
return MangasPage(ret, false)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val res = JSONObject(response.body()!!.string())
|
||||
val volumes = res.getJSONObject("volumes")
|
||||
val ret = mutableListOf<SChapter>()
|
||||
volumes.keys().forEach { vol ->
|
||||
val chapters = volumes.getJSONObject(vol)
|
||||
chapters.keys().forEach { ch ->
|
||||
val obj = chapters.getJSONObject(ch)
|
||||
obj.put("chapter", ch)
|
||||
obj.put("volume", vol)
|
||||
ret.add(SChapter.create().apply { fromJSON(obj) })
|
||||
}
|
||||
}
|
||||
return ret.sortedByDescending { it.name }
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
SManga.create().apply {
|
||||
fromJSON(JSONObject(response.body()!!.string()))
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val obj = JSONObject(response.body()!!.string())
|
||||
val url = obj.getString("url")
|
||||
val root = obj.getString("pages_root")
|
||||
val arr = obj.getJSONArray("pages_list")
|
||||
val ret = mutableListOf<Page>()
|
||||
for (i in 0 until arr.length()) {
|
||||
ret.add(Page(i, "$url${i + 1}", root + arr.getString(i)))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val arr = JSONArray(response.body()!!.string())
|
||||
val ret = mutableListOf<SManga>()
|
||||
for (i in 0 until arr.length()) {
|
||||
ret.add(SManga.create().apply {
|
||||
fromJSON(arr.getJSONObject(i))
|
||||
})
|
||||
}
|
||||
return MangasPage(ret.sortedBy { it.title }, false)
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Person(), Status(), CategoryList()
|
||||
)
|
||||
|
||||
override fun fetchPopularManga(page: Int) =
|
||||
fetchSearchManga(page, "", FilterList())
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!")
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!")
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user