Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/en/mangalix/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import io.github.keiyoushi.gradle.api.ContentWarning

plugins {
alias(kei.plugins.extension)
}

keiyoushi {
name = "MangaLix"
versionCode = 1
contentWarning = ContentWarning.MIXED
libVersion = "1.6"

source {
lang = "en"
baseUrl = "https://mangalix.com"
}

deeplink {
path("/manga/..*")
}
}
Binary file added src/en/mangalix/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/en/mangalix/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/en/mangalix/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/en/mangalix/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.extension.en.mangalix

import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
import java.util.Locale

@Serializable
internal class MangaDto(
val slug: String,
val title: String,
val description: String,
val coverImage: String,
val author: String,
val status: String,
val rating: Double,
val releaseYear: Double,
val genres: List<String>,
val latestChapter: LatestChapterDto?,
) {
val latestTimestamp: Long
get() = latestChapter?.releaseDate.toMangaTimestamp()

fun toSManga(baseUrl: String): SManga = SManga.create().apply {
url = slug
title = this@MangaDto.title
thumbnail_url = coverImage.takeIf { it.isNotBlank() }?.let {
when {
it.startsWith("http://", ignoreCase = true) || it.startsWith("https://", ignoreCase = true) -> it
it.startsWith("//") -> "https:$it"
else -> "${baseUrl.trimEnd('/')}/${it.trimStart('/')}"
}
}
author = this@MangaDto.author.takeIf { it.isNotBlank() }
description = this@MangaDto.description.takeIf { it.isNotBlank() }
genre = genres.takeIf { it.isNotEmpty() }?.joinToString()
status = this@MangaDto.status.toMangaStatus()
initialized = true
}
}

@Serializable
internal class LatestChapterDto(
val releaseDate: String?,
)

internal class ChapterDto(
val id: String,
val number: Double,
val title: String,
val pages: List<String>,
val releaseDate: String? = null,
)

internal fun String?.toMangaTimestamp(): Long = this?.let { value ->
runCatching { Instant.parse(value).toEpochMilli() }
.recoverCatching { LocalDate.parse(value).atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli() }
.getOrDefault(0L)
} ?: 0L

private fun String.toMangaStatus(): Int = when (
lowercase(Locale.ROOT)
.trim()
.replace("-", " ")
.replace(Regex("""\s+"""), " ")
) {
"ongoing", "publishing", "releasing", "active" -> SManga.ONGOING
"completed", "complete", "finished" -> SManga.COMPLETED
"hiatus", "on hiatus", "paused" -> SManga.ON_HIATUS
"cancelled", "canceled", "dropped", "axed", "discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package eu.kanade.tachiyomi.extension.en.mangalix

import eu.kanade.tachiyomi.source.model.Filter
import java.util.Locale

internal class StatusFilter :
Filter.Select<String>(
"Status",
STATUS_OPTIONS.map { it.first }.toTypedArray(),
) {
val selected: String?
get() = STATUS_OPTIONS[state].second.takeIf { it.isNotEmpty() }

fun matches(status: String): Boolean = selected?.let {
status.normalizedStatus() == it
} ?: true

companion object {
private val STATUS_OPTIONS = listOf(
"All" to "",
"Ongoing" to "ongoing",
"Completed" to "completed",
"Hiatus" to "hiatus",
)
}
}

internal class SortFilter(
selection: Selection = Selection(0, false),
) : Filter.Sort(
"Sort By",
SORT_OPTIONS.map { it.first }.toTypedArray(),
selection,
) {
val selected: String
get() = SORT_OPTIONS[state?.index ?: 0].second

val ascending: Boolean
get() = state?.ascending ?: false

companion object {
const val DEFAULT = "default"
const val LATEST = "latest"
const val RATING = "rating"
const val TITLE = "title"
const val RELEASE_YEAR = "release_year"

private val SORT_OPTIONS = listOf(
"Default" to DEFAULT,
"Latest Update" to LATEST,
"Release Year" to RELEASE_YEAR,
"Rating" to RATING,
"Title" to TITLE,
)
}
}

internal class GenreFilter :
Filter.Group<GenreOption>(
"Genres",
GENRES.map(::GenreOption),
) {
val selectedGenres: Set<String>
get() = state.filter { it.state }.mapTo(linkedSetOf()) { it.name }

fun matches(genres: List<String>): Boolean {
val selected = selectedGenres.mapTo(hashSetOf()) { it.normalizedGenre() }
if (selected.isEmpty()) return true

val available = genres.mapTo(hashSetOf()) { it.normalizedGenre() }
return available.containsAll(selected)
}

companion object {
private val GENRES = listOf(
"4-Koma",
"Action",
"Adventure",
"Comedy",
"Dark Fantasy",
"Drama",
"Ecchi",
"Family",
"Fantasy",
"Harem",
"Historical",
"Horror",
"Isekai",
"Magic",
"Manhwa",
"Martial Arts",
"Mature",
"Mecha",
"Military",
"Murim",
"Mystery",
"Parody",
"Psychological",
"Regression",
"Reincarnation",
"Romance",
"School Life",
"Sci-Fi",
"Seinen",
"Shoujo",
"Shounen",
"Slice of Life",
"Sports",
"Supernatural",
"Survival",
"System",
"Thriller",
"Tragedy",
"Vampire",
"Webtoon",
)
}
}

internal class GenreOption(name: String) : Filter.CheckBox(name)

private fun String.normalizedStatus(): String = when (normalizedKey()) {
"ongoing", "publishing", "releasing", "active" -> "ongoing"
"completed", "complete", "finished" -> "completed"
"hiatus", "onhiatus", "paused" -> "hiatus"
else -> normalizedKey()
}

private fun String.normalizedGenre(): String = when (val value = normalizedKey()) {
"school" -> "schoollife"
"shojo" -> "shoujo"
"shonen" -> "shounen"
"webtoons" -> "webtoon"
else -> value
}

private fun String.normalizedKey(): String = lowercase(Locale.ROOT)
.filter(Char::isLetterOrDigit)
Loading