diff --git a/src/en/latisbooks/build.gradle b/src/en/latisbooks/build.gradle deleted file mode 100644 index 53084f1e5..000000000 --- a/src/en/latisbooks/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - extName = 'Latis Books' - extClass = '.Latisbooks' - extVersionCode = 6 - isNsfw = true -} - -apply from: "$rootDir/common.gradle" diff --git a/src/en/latisbooks/res/mipmap-hdpi/ic_launcher.png b/src/en/latisbooks/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 37d95aac4..000000000 Binary files a/src/en/latisbooks/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/latisbooks/res/mipmap-mdpi/ic_launcher.png b/src/en/latisbooks/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index edd1522b7..000000000 Binary files a/src/en/latisbooks/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/latisbooks/res/mipmap-xhdpi/ic_launcher.png b/src/en/latisbooks/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index a5d4f2602..000000000 Binary files a/src/en/latisbooks/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/latisbooks/res/mipmap-xxhdpi/ic_launcher.png b/src/en/latisbooks/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 66b559ff8..000000000 Binary files a/src/en/latisbooks/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/latisbooks/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/latisbooks/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 13120621e..000000000 Binary files a/src/en/latisbooks/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/latisbooks/src/eu/kanade/tachiyomi/extension/en/latisbooks/Latisbooks.kt b/src/en/latisbooks/src/eu/kanade/tachiyomi/extension/en/latisbooks/Latisbooks.kt deleted file mode 100644 index 664737153..000000000 --- a/src/en/latisbooks/src/eu/kanade/tachiyomi/extension/en/latisbooks/Latisbooks.kt +++ /dev/null @@ -1,146 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.latisbooks - -import android.net.Uri.encode -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.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.util.asJsoup -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import rx.Observable -import java.util.Calendar - -class Latisbooks : HttpSource() { - - override val name = "Latis Books" - - override val baseUrl = "https://www.latisbooks.com" - - override val lang = "en" - - override val supportsLatest = false - - override val client: OkHttpClient = network.cloudflareClient - - private val textToImageURL = "https://fakeimg.ryd.tools/1500x2126/ffffff/000000/?font=museo&font_size=42" - - private fun String.image() = textToImageURL + "&text=" + encode(this) - - private fun createManga(response: Response): SManga { - return SManga.create().apply { - initialized = true - title = "Bodysuit 23" - url = "/archive/" - thumbnail_url = "https://images.squarespace-cdn.com/content/v1/56595108e4b01110e1cf8735/1511856223610-NSB8O5OJ1F6KPQL0ZGBH/image-asset.jpeg" - } - } - - // Popular - - override fun fetchPopularManga(page: Int): Observable { - return client.newCall(popularMangaRequest(page)) - .asObservableSuccess() - .map { response -> - MangasPage(listOf(createManga(response)), false) - } - } - - override fun popularMangaRequest(page: Int): Request { - return (GET("$baseUrl/archive/", headers)) - } - - override fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() - - // Latest - - override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() - - override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() - - // Search - - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.just(MangasPage(emptyList(), false)) - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException() - - override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() - - // Details - - override fun fetchMangaDetails(manga: SManga): Observable { - return client.newCall(mangaDetailsRequest(manga)) - .asObservableSuccess() - .map { response -> - createManga(response) - } - } - - override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException() - - // Chapters - - override fun chapterListParse(response: Response): List { - val cal: Calendar = Calendar.getInstance() - - return response.asJsoup().select("ul.archive-item-list li a").map { - val date: List = it.attr("abs:href").split("/") - cal.set(date[4].toInt(), date[5].toInt() - 1, date[6].toInt()) - - SChapter.create().apply { - name = it.text() - url = it.attr("abs:href") - date_upload = cal.timeInMillis - } - } - } - - // Pages - - // Adapted from the xkcd source's wordWrap function - private fun wordWrap(text: String) = buildString { - var charCount = 0 - text.replace("\r\n", " ").split(' ').forEach { w -> - if (charCount > 25) { - append("\n") - charCount = 0 - } - append(w).append(' ') - charCount += w.length + 1 - } - } - - override fun pageListRequest(chapter: SChapter): Request = GET(chapter.url, headers) - - override fun pageListParse(response: Response): List { - val blocks = response.asJsoup().select("div.content-wrapper div.row div.col") - - // Handle multiple images per page (e.g. Page 23+24) - val pages = blocks.select("div.image-block-wrapper img") - .mapIndexed { i, it -> Page(i, "", it.attr("abs:data-src")) } - .toMutableList() - - val numImages = pages.size - - // Add text above/below the image as xkcd-esque text pages after the image itself - pages.addAll( - blocks.select("div.html-block") - .map { it.select("div.sqs-block-content").first()!! } - // Some pages have empty html blocks (e.g. Page 1), so ignore them - .filter { it.childrenSize() > 0 } - .mapIndexed { i, it -> Page(i + numImages, "", wordWrap(it.text()).image()) } - .toList(), - ) - - return pages.toList() - } - - override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() - - override fun getFilterList() = FilterList() -} diff --git a/src/en/mehgazone/build.gradle b/src/en/mehgazone/build.gradle new file mode 100644 index 000000000..572f02dea --- /dev/null +++ b/src/en/mehgazone/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Mehgazone' + extClass = '.Mehgazone' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/mehgazone/res/mipmap-hdpi/ic_launcher.png b/src/en/mehgazone/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..6c9ad51a4 Binary files /dev/null and b/src/en/mehgazone/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/mehgazone/res/mipmap-mdpi/ic_launcher.png b/src/en/mehgazone/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..927e647d8 Binary files /dev/null and b/src/en/mehgazone/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/mehgazone/res/mipmap-xhdpi/ic_launcher.png b/src/en/mehgazone/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..cd339d62b Binary files /dev/null and b/src/en/mehgazone/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/mehgazone/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mehgazone/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..db50d17d9 Binary files /dev/null and b/src/en/mehgazone/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/mehgazone/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mehgazone/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5f43639d6 Binary files /dev/null and b/src/en/mehgazone/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/Mehgazone.kt b/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/Mehgazone.kt new file mode 100644 index 000000000..84e9add7e --- /dev/null +++ b/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/Mehgazone.kt @@ -0,0 +1,330 @@ +package eu.kanade.tachiyomi.extension.en.mehgazone + +import android.content.SharedPreferences +import android.text.InputType +import android.text.SpannableString +import android.text.method.LinkMovementMethod +import android.text.util.Linkify +import android.util.Log +import android.view.ViewGroup +import android.widget.EditText +import android.widget.TextView +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.extension.en.mehgazone.interceptors.BasicAuthInterceptor +import eu.kanade.tachiyomi.extension.en.mehgazone.serialization.ChapterListDto +import eu.kanade.tachiyomi.extension.en.mehgazone.serialization.PageListDto +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.ConfigurableSource +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.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.utils.getPreferencesLazy +import keiyoushi.utils.parseAs +import keiyoushi.utils.tryParse +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.helper.Validate +import org.jsoup.nodes.Element +import org.jsoup.parser.Parser.unescapeEntities +import org.jsoup.select.Collector +import org.jsoup.select.Elements +import org.jsoup.select.QueryParser +import rx.Observable +import java.text.SimpleDateFormat +import java.util.Locale + +class Mehgazone : ConfigurableSource, HttpSource() { + + override val name = "Mehgazone" + + override val baseUrl = "https://mehgazone.com" + + override val lang = "en" + + override val supportsLatest = false + + override val client: OkHttpClient by lazy { + network.cloudflareClient + .newBuilder() + .addInterceptor(authInterceptor) + .build() + } + + private val uploadDateFormat: SimpleDateFormat by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US) + } + + private val textToImageURL = "https://fakeimg.ryd.tools/1500x2126/ffffff/000000/?font=museo&font_size=42".toHttpUrl() + + private fun String.image() = textToImageURL.newBuilder().setQueryParameter("text", this).build().toString() + + private fun String.unescape() = unescapeEntities(this, false) + + private fun String.linkify() = SpannableString(this).apply { Linkify.addLinks(this, Linkify.WEB_URLS) } + + override fun popularMangaRequest(page: Int) = GET(baseUrl, headers) + + override fun getMangaUrl(manga: SManga) = manga.url + + private fun Elements.selectFirstBackport(cssQuery: String) = selectFirst(cssQuery, this) + + // backport from jsoup 1.19.1 + private fun selectFirst(cssQuery: String, roots: Elements): Element? { + Validate.notEmpty(cssQuery) + Validate.notNull(roots) + val evaluator = QueryParser.parse(cssQuery) + + for (root in roots) { + val first = Collector.findFirst(evaluator, root) + if (first != null) return first + } + + return null + } + + override fun popularMangaParse(response: Response) = MangasPage( + response.asJsoup() + .selectFirst("#main aside.primary-sidebar .sidebar-group")!! + .select("h2") + .filter { el -> el.text().contains("Latest", true) } + .map { + SManga.create().apply { + title = it.text().split('"')[1].unescape() + url = it.nextElementSiblings().selectFirstBackport("a[href*='/feed']")!!.attr("href").toHttpUrl().resolve("/").toString() + thumbnail_url = it.nextElementSiblings().selectFirstBackport("img")!!.attr("src") + } + }, + false, + ) + + override fun mangaDetailsRequest(manga: SManga) = + GET(manga.url, headers) + + override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply { + val html = response.asJsoup() + val thumbnailRegex = Regex("/[^/]+-([0-9]+\\.png)\$", RegexOption.IGNORE_CASE) + + title = html.head().selectFirst("title")!!.text().unescape() + url = response.request.url.toString() + author = "Patricia Barton" + status = SManga.ONGOING + thumbnail_url = + html.select("#content img[src*='.png']") + .firstOrNull { it.attr("src").matches(thumbnailRegex) } + ?.attr("src") + ?.replace(thumbnailRegex, "/\$1") + } + + override fun chapterListRequest(manga: SManga): Request = chapterListRequest(manga.url, 1) + + private fun chapterListRequest(url: String, page: Int): Request = + GET( + "$url/wp-json/wp/v2/posts?per_page=100&page=$page&_fields=id,title,date_gmt,excerpt", + headers, + ) + + private fun hasNextPage(headers: Headers, responseSize: Int, page: Int): Boolean { + val pages = headers["X-Wp-Totalpages"]?.toInt() + ?: return responseSize == 100 + return page < pages + } + + override fun getChapterUrl(chapter: SChapter): String = chapter.url + + override fun chapterListParse(response: Response): List { + val apiResponse = response.parseAs>().toMutableList() + val mangaUrl = response.request.url.toString().substringBefore("/wp-json/") + + if (hasNextPage(response.headers, apiResponse.size, 1)) { + var page = 1 + do { + page++ + val tempResponse = client.newCall(chapterListRequest(mangaUrl, page)).execute() + val headers = tempResponse.headers + val tempApiResponse = tempResponse.parseAs>() + + apiResponse.addAll(tempApiResponse) + tempResponse.close() + } while (hasNextPage(headers, tempApiResponse.size, page)) + } + + return apiResponse + .filter { !it.excerpt.rendered.contains("Unlock with Patreon") } + .distinctBy { it.id } + .sortedBy { it.date } + .mapIndexed { i, it -> + SChapter.create().apply { + url = "$mangaUrl/?p=${it.id}" + name = it.title.rendered.unescape() + .ifEmpty { it.date.substringBefore('T') } + date_upload = uploadDateFormat.tryParse(it.date) + chapter_number = i.toFloat() + } + }.reversed() + } + + // Adapted from the xkcd source's wordWrap function + private fun wordWrap(text: String) = buildString { + var charCount = 0 + text.replace('\n', ' ').split(' ').forEach { w -> + if (charCount > 25) { + append("\n") + charCount = 0 + } + append(w).append(' ') + charCount += w.length + 1 + } + } + + override fun pageListRequest(chapter: SChapter): Request { + val chapterUrl = chapter.url.toHttpUrl() + val pageListUrl = chapterUrl + .newBuilder("/wp-json/wp/v2/posts?per_page=1&_fields=link,content,excerpt,date,title")!! + .setQueryParameter("include", chapterUrl.queryParameter("p")) + .build() + return GET(pageListUrl.toString(), headers) + } + + override fun pageListParse(response: Response): List { + val apiResponse: PageListDto = response.parseAs>().first() + + val content = Jsoup.parseBodyFragment(apiResponse.content.rendered, apiResponse.link) + + val images = content.select("img") + .mapIndexed { i, it -> Page(i, "", it.attr("src")) } + .toMutableList() + + if (apiResponse.excerpt.rendered.isNotBlank()) { + images.add( + Page( + images.size, + "", + wordWrap(Jsoup.parseBodyFragment(apiResponse.excerpt.rendered.unescape()).text()).image(), + ), + ) + } + + return images.toList() + } + + private val preferences: SharedPreferences by getPreferencesLazy() + + companion object { + private const val WORDPRESS_USERNAME_PREF_KEY = "WORDPRESS_USERNAME" + private const val WORDPRESS_USERNAME_PREF_TITLE = "WordPress username" + private const val WORDPRESS_USERNAME_PREF_SUMMARY = "The WordPress username" + private const val WORDPRESS_USERNAME_PREF_DIALOG = "To see your username:\n\n" + + "Go to https://bodysuit23.mehgazone.com/wp-admin/profile.php and you should see your username near the top of the page." + private const val WORDPRESS_USERNAME_PREF_DEFAULT_VALUE = "" + + private const val WORDPRESS_APP_PASSWORD_PREF_KEY = "WORDPRESS_APP_PASSWORD" + private const val WORDPRESS_APP_PASSWORD_PREF_TITLE = "WordPress app password" + private const val WORDPRESS_APP_PASSWORD_PREF_SUMMARY = "The WordPress app password (not your account password)" + private const val WORDPRESS_APP_PASSWORD_PREF_DIALOG = "To setup:\n\n" + + "Go to https://bodysuit23.mehgazone.com/wp-admin/profile.php and you should be able to create a new app password near the bottom of the page." + private const val WORDPRESS_APP_PASSWORD_PREF_DEFAULT_VALUE = "" + } + + private val authInterceptor: BasicAuthInterceptor by lazy { + BasicAuthInterceptor( + preferences.getString(WORDPRESS_USERNAME_PREF_KEY, WORDPRESS_USERNAME_PREF_DEFAULT_VALUE), + preferences.getString(WORDPRESS_APP_PASSWORD_PREF_KEY, WORDPRESS_APP_PASSWORD_PREF_DEFAULT_VALUE), + ) + } + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + EditTextPreference(screen.context).apply { + val name = preferences.getString(WORDPRESS_USERNAME_PREF_KEY, WORDPRESS_USERNAME_PREF_DEFAULT_VALUE)!! + + key = WORDPRESS_USERNAME_PREF_KEY + title = WORDPRESS_USERNAME_PREF_TITLE + dialogMessage = WORDPRESS_USERNAME_PREF_DIALOG.linkify() + summary = name.ifBlank { WORDPRESS_USERNAME_PREF_SUMMARY } + setDefaultValue(WORDPRESS_USERNAME_PREF_DEFAULT_VALUE) + + setOnBindEditTextListener { + getDialogMessageFromEditText(it).let { + @Suppress("NestedLambdaShadowedImplicitParameter") + if (it == null) { + Log.e(name, "Could not find dialog TextView") + } else { + it.movementMethod = LinkMovementMethod.getInstance() + } + } + } + + setOnPreferenceChangeListener { preference, newValue -> + authInterceptor.setUser(newValue as String) + preference.summary = newValue.ifBlank { WORDPRESS_USERNAME_PREF_SUMMARY } + true + } + }.also(screen::addPreference) + + EditTextPreference(screen.context).apply { + val pwd = preferences.getString(WORDPRESS_APP_PASSWORD_PREF_KEY, WORDPRESS_APP_PASSWORD_PREF_DEFAULT_VALUE)!! + + key = WORDPRESS_APP_PASSWORD_PREF_KEY + title = WORDPRESS_APP_PASSWORD_PREF_TITLE + dialogMessage = WORDPRESS_APP_PASSWORD_PREF_DIALOG.linkify() + summary = if (pwd.isBlank()) WORDPRESS_APP_PASSWORD_PREF_SUMMARY else "●".repeat(pwd.length) + setDefaultValue(WORDPRESS_APP_PASSWORD_PREF_DEFAULT_VALUE) + + setOnBindEditTextListener { + it.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + getDialogMessageFromEditText(it).let { + @Suppress("NestedLambdaShadowedImplicitParameter") + if (it == null) { + Log.e(name, "Could not find dialog TextView") + } else { + it.movementMethod = LinkMovementMethod.getInstance() + } + } + } + + setOnPreferenceChangeListener { preference, newValue -> + authInterceptor.setPassword(newValue as String) + preference.summary = if (newValue.isBlank()) WORDPRESS_APP_PASSWORD_PREF_SUMMARY else "●".repeat(newValue.length) + true + } + }.also(screen::addPreference) + } + + private fun getDialogMessageFromEditText(editText: EditText): TextView? { + val parent = editText.parent + if (parent !is ViewGroup || parent.childCount == 0) return null + + for (i in 1..parent.childCount) { + val child = parent.getChildAt(i - 1) + if (child is TextView && child !is EditText) return child + } + + return null + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = + fetchPopularManga(0).map { + MangasPage( + it.mangas.filter { m -> m.title.contains(query) }, + false, + ) + } + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException() + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException() + + override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() +} diff --git a/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/interceptors/BasicAuthInterceptor.kt b/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/interceptors/BasicAuthInterceptor.kt new file mode 100644 index 000000000..e8a5e5471 --- /dev/null +++ b/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/interceptors/BasicAuthInterceptor.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.extension.en.mehgazone.interceptors + +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.Response + +class BasicAuthInterceptor(private var user: String?, private var password: String?) : Interceptor { + fun setUser(user: String?) { + setAuth(user, password) + } + + fun setPassword(password: String?) { + setAuth(user, password) + } + + fun setAuth(user: String?, password: String?) { + this.user = user + this.password = password + credentials = getCredentials() + } + + private fun getCredentials(): String? = + if (!user.isNullOrBlank() && !password.isNullOrBlank()) { + Credentials.basic(user!!, password!!) + } else { + null + } + + private var credentials: String? = getCredentials() + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + if ( + !request.url.encodedPath.contains("/wp-json/wp/v2/") || + user.isNullOrBlank() || + password.isNullOrBlank() || + credentials.isNullOrBlank() + ) { + return chain.proceed(request) + } + + val authenticatedRequest = request.newBuilder() + .header("Authorization", credentials!!) + .build() + + return chain.proceed(authenticatedRequest) + } +} diff --git a/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/serialization/Dto.kt b/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/serialization/Dto.kt new file mode 100644 index 000000000..0fe6a1ef9 --- /dev/null +++ b/src/en/mehgazone/src/eu/kanade/tachiyomi/extension/en/mehgazone/serialization/Dto.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.extension.en.mehgazone.serialization + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class ChapterListDto( + val id: Int, + @SerialName("date_gmt") + val date: String, + val title: RenderedDto, + val excerpt: RenderedDto, +) + +@Serializable +class PageListDto( + val link: String, + val content: RenderedDto, + val excerpt: RenderedDto, +) + +@Serializable +class RenderedDto( + val rendered: String, +)