diff --git a/src/en/allanime/AndroidManifest.xml b/src/en/allanime/AndroidManifest.xml index 581bf58ca..99893d22f 100644 --- a/src/en/allanime/AndroidManifest.xml +++ b/src/en/allanime/AndroidManifest.xml @@ -2,7 +2,7 @@ @@ -12,7 +12,7 @@ - + diff --git a/src/en/allanime/build.gradle b/src/en/allanime/build.gradle index 20752a75b..a4d0180a3 100644 --- a/src/en/allanime/build.gradle +++ b/src/en/allanime/build.gradle @@ -1,7 +1,7 @@ ext { - extName = 'AllAnime' - extClass = '.AllAnime' - extVersionCode = 6 + extName = 'AllManga' + extClass = '.AllManga' + extVersionCode = 7 } apply from: "$rootDir/common.gradle" diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeHelper.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeHelper.kt deleted file mode 100644 index f579c4493..000000000 --- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeHelper.kt +++ /dev/null @@ -1,81 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.allanime - -import eu.kanade.tachiyomi.source.model.SManga -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import okhttp3.Headers -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response -import org.jsoup.Jsoup -import java.text.SimpleDateFormat -import java.util.Locale - -object AllAnimeHelper { - - val json: Json = Json { - ignoreUnknownKeys = true - explicitNulls = false - encodeDefaults = true - coerceInputValues = true - } - - fun String.parseThumbnailUrl(): String { - return if (this.matches(AllAnime.urlRegex)) { - this - } else { - "$thumbnail_cdn$this?w=250" - } - } - - fun String?.parseStatus(): Int { - if (this == null) { - return SManga.UNKNOWN - } - - return when { - this.contains("releasing", true) -> SManga.ONGOING - this.contains("finished", true) -> SManga.COMPLETED - else -> SManga.UNKNOWN - } - } - - fun String.titleToSlug() = this.trim() - .lowercase(Locale.US) - .replace(titleSpecialCharactersRegex, "-") - - fun String.parseDescription(): String { - return Jsoup.parse( - this.replace("
", "br2n"), - ).text().replace("br2n", "\n") - } - - fun String?.parseDate(): Long { - return runCatching { - dateFormat.parse(this!!)!!.time - }.getOrDefault(0L) - } - - inline fun Response.parseAs(): T = json.decodeFromString(body.string()) - - inline fun List<*>.firstInstanceOrNull(): T? = - filterIsInstance().firstOrNull() - - inline fun T.toJsonRequestBody(): RequestBody = - json.encodeToString(this) - .toRequestBody(JSON_MEDIA_TYPE) - - fun Headers.Builder.buildApiHeaders(requestBody: RequestBody) = this - .add("Content-Length", requestBody.contentLength().toString()) - .add("Content-Type", requestBody.contentType().toString()) - .build() - - private const val thumbnail_cdn = "https://wp.youtube-anime.com/aln.youtube-anime.com/" - private val titleSpecialCharactersRegex by lazy { Regex("[^a-z\\d]+") } - private val dateFormat by lazy { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) - } - val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() -} diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimePayloadDto.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimePayloadDto.kt deleted file mode 100644 index 7f53253a1..000000000 --- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimePayloadDto.kt +++ /dev/null @@ -1,59 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.allanime - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class GraphQL( - val variables: T, - val query: String, -) - -@Serializable -data class PopularVariables( - val type: String, - val size: Int, - val dateRange: Int, - val page: Int, - val allowAdult: Boolean, - val allowUnknown: Boolean, -) - -@Serializable -data class SearchVariables( - val search: SearchPayload, - @SerialName("limit") val size: Int, - val page: Int, - val translationType: String, - val countryOrigin: String, -) - -@Serializable -data class SearchPayload( - val query: String?, - val sortBy: String?, - val genres: List?, - val excludeGenres: List?, - val isManga: Boolean, - val allowAdult: Boolean, - val allowUnknown: Boolean, -) - -@Serializable -data class IDVariables( - val id: String, -) - -@Serializable -data class ChapterListVariables( - val id: String, - val chapterNumStart: Float, - val chapterNumEnd: Float, -) - -@Serializable -data class PageListVariables( - val id: String, - val chapterNum: String, - val translationType: String, -) diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnime.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllManga.kt similarity index 80% rename from src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnime.kt rename to src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllManga.kt index a988e10e4..b3d49e1d2 100644 --- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnime.kt +++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllManga.kt @@ -5,10 +5,7 @@ import android.content.SharedPreferences import androidx.preference.ListPreference import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.buildApiHeaders -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.firstInstanceOrNull -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseAs -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.toJsonRequestBody +import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.interceptor.rateLimit @@ -26,16 +23,18 @@ import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class AllAnime : ConfigurableSource, HttpSource() { +class AllManga : ConfigurableSource, HttpSource() { - override val name = "AllAnime" + override val name = "AllManga" - override val baseUrl = "https://allanime.ai" + override val baseUrl = "https://allmanga.to" private val apiUrl = "https://api.allanime.day/api" override val lang = "en" + override val id = 4709139914729853090 + override val supportsLatest = true private val preferences by lazy { @@ -43,24 +42,6 @@ class AllAnime : ConfigurableSource, HttpSource() { } override val client = network.cloudflareClient.newBuilder() - .addInterceptor { chain -> - val request = chain.request() - val frag = request.url.fragment - val quality = preferences.imageQuality - - if (frag.isNullOrEmpty() || quality == IMAGE_QUALITY_PREF_DEFAULT) { - return@addInterceptor chain.proceed(request) - } - - val oldUrl = request.url.toString() - val newUrl = oldUrl.replace(imageQualityRegex, "$image_cdn/$1?w=$quality") - - return@addInterceptor chain.proceed( - request.newBuilder() - .url(newUrl) - .build(), - ) - } .rateLimit(1) .build() @@ -72,7 +53,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val payload = GraphQL( PopularVariables( type = "manga", - size = limit, + size = LIMIT, dateRange = 0, page = page, allowAdult = preferences.allowAdult, @@ -83,9 +64,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val requestBody = payload.toJsonRequestBody() - val apiHeaders = headersBuilder().buildApiHeaders(requestBody) - - return POST(apiUrl, apiHeaders, requestBody) + return POST(apiUrl, headers, requestBody) } override fun popularMangaParse(response: Response): MangasPage { @@ -94,7 +73,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val mangaList = result.data.popular.mangas .mapNotNull { it.manga?.toSManga() } - val hasNextPage = result.data.popular.mangas.size == limit + val hasNextPage = result.data.popular.mangas.size == LIMIT return MangasPage(mangaList, hasNextPage) } @@ -128,7 +107,7 @@ class AllAnime : ConfigurableSource, HttpSource() { allowAdult = preferences.allowAdult, allowUnknown = false, ), - size = limit, + size = LIMIT, page = page, translationType = "sub", countryOrigin = filters.firstInstanceOrNull()?.getValue() ?: "ALL", @@ -138,9 +117,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val requestBody = payload.toJsonRequestBody() - val apiHeaders = headersBuilder().buildApiHeaders(requestBody) - - return POST(apiUrl, apiHeaders, requestBody) + return POST(apiUrl, headers, requestBody) } override fun searchMangaParse(response: Response): MangasPage { @@ -149,7 +126,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val mangaList = result.data.mangas.edges .map(SearchManga::toSManga) - val hasNextPage = result.data.mangas.edges.size == limit + val hasNextPage = result.data.mangas.edges.size == LIMIT return MangasPage(mangaList, hasNextPage) } @@ -167,9 +144,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val requestBody = payload.toJsonRequestBody() - val apiHeaders = headersBuilder().buildApiHeaders(requestBody) - - return POST(apiUrl, apiHeaders, requestBody) + return POST(apiUrl, headers, requestBody) } override fun mangaDetailsParse(response: Response): SManga { @@ -205,9 +180,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val requestBody = payload.toJsonRequestBody() - val apiHeaders = headersBuilder().buildApiHeaders(requestBody) - - return POST(apiUrl, apiHeaders, requestBody) + return POST(apiUrl, headers, requestBody) } private fun chapterListParse(response: Response, manga: SManga): List { @@ -246,9 +219,7 @@ class AllAnime : ConfigurableSource, HttpSource() { val requestBody = payload.toJsonRequestBody() - val apiHeaders = headersBuilder().buildApiHeaders(requestBody) - - return POST(apiUrl, apiHeaders, requestBody) + return POST(apiUrl, headers, requestBody) } override fun pageListParse(response: Response): List { @@ -266,11 +237,24 @@ class AllAnime : ConfigurableSource, HttpSource() { return pages.pictureUrls?.mapIndexed { index, image -> Page( index = index, - imageUrl = "$imageDomain${image.url}#page", + imageUrl = "$imageDomain${image.url}", ) } ?: emptyList() } + override fun imageRequest(page: Page): Request { + val quality = preferences.imageQuality + + if (quality == IMAGE_QUALITY_PREF_DEFAULT) { + return super.imageRequest(page) + } + + val oldUrl = imageQualityRegex.find(page.imageUrl!!)!!.groupValues[1] + val newUrl = "$IMAGE_CDN/$oldUrl?w=$quality" + + return GET(newUrl, headers) + } + override fun imageUrlParse(response: Response): String { throw UnsupportedOperationException() } @@ -299,11 +283,11 @@ class AllAnime : ConfigurableSource, HttpSource() { get() = getString(IMAGE_QUALITY_PREF, IMAGE_QUALITY_PREF_DEFAULT)!! companion object { - private const val limit = 20 + private const val LIMIT = 20 const val SEARCH_PREFIX = "id:" val urlRegex = Regex("^https?://.*") - private const val image_cdn = "https://wp.youtube-anime.com" - private val imageQualityRegex = Regex("^https?://(.*)#.*") + private const val IMAGE_CDN = "https://wp.youtube-anime.com" + private val imageQualityRegex = Regex("^https?://([^#]+)") private const val SHOW_ADULT_PREF = "pref_adult" private const val SHOW_ADULT_PREF_DEFAULT = false diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeUrlActivity.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllMangaUrlActivity.kt similarity index 79% rename from src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeUrlActivity.kt rename to src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllMangaUrlActivity.kt index 7c844a846..815fd029a 100644 --- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeUrlActivity.kt +++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllMangaUrlActivity.kt @@ -7,7 +7,7 @@ import android.os.Bundle import android.util.Log import kotlin.system.exitProcess -class AllAnimeUrlActivity : Activity() { +class AllMangaUrlActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val pathSegments = intent?.data?.pathSegments @@ -15,17 +15,17 @@ class AllAnimeUrlActivity : Activity() { val id = pathSegments[1] val mainIntent = Intent().apply { action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${AllAnime.SEARCH_PREFIX}$id") + putExtra("query", "${AllManga.SEARCH_PREFIX}$id") putExtra("filter", packageName) } try { startActivity(mainIntent) } catch (e: ActivityNotFoundException) { - Log.e("AllAnimeUrlActivity", e.toString()) + Log.e("AllMangaUrlActivity", e.toString()) } } else { - Log.e("AllAnimeUrlActivity", "could not parse uri from intent $intent") + Log.e("AllMangaUrlActivity", "could not parse uri from intent $intent") } finish() diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeDto.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Dto.kt similarity index 70% rename from src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeDto.kt rename to src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Dto.kt index 3c6ffd281..49eac0e2d 100644 --- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeDto.kt +++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Dto.kt @@ -1,10 +1,5 @@ package eu.kanade.tachiyomi.extension.en.allanime -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseDate -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseDescription -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseStatus -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.parseThumbnailUrl -import eu.kanade.tachiyomi.extension.en.allanime.AllAnimeHelper.titleToSlug import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import kotlinx.serialization.SerialName @@ -22,39 +17,39 @@ typealias ApiChapterListResponse = Data typealias ApiPageListResponse = Data @Serializable -data class Data(val data: T) +class Data(val data: T) @Serializable -data class Edges(val edges: List) +class Edges(val edges: List) // Popular @Serializable -data class PopularData( +class PopularData( @SerialName("queryPopular") val popular: PopularMangas, ) @Serializable -data class PopularMangas( +class PopularMangas( @SerialName("recommendations") val mangas: List, ) @Serializable -data class PopularManga( +class PopularManga( @SerialName("anyCard") val manga: SearchManga? = null, ) // Search @Serializable -data class SearchData( +class SearchData( val mangas: Edges, ) @Serializable -data class SearchManga( +class SearchManga( @SerialName("_id") val id: String, - val name: String, - val thumbnail: String? = null, - val englishName: String? = null, + private val name: String, + private val thumbnail: String? = null, + private val englishName: String? = null, ) { fun toSManga() = SManga.create().apply { title = englishName ?: name @@ -65,22 +60,22 @@ data class SearchManga( // Details @Serializable -data class MangaDetailsData( +class MangaDetailsData( val manga: Manga, ) @Serializable -data class Manga( +class Manga( @SerialName("_id") val id: String, - val name: String, - val thumbnail: String? = null, - val description: String? = null, - val authors: List? = emptyList(), - val genres: List? = emptyList(), - val tags: List? = emptyList(), - val status: String? = null, - val altNames: List? = emptyList(), - val englishName: String? = null, + private val name: String, + private val thumbnail: String? = null, + private val description: String? = null, + private val authors: List? = emptyList(), + private val genres: List? = emptyList(), + private val tags: List? = emptyList(), + private val status: String? = null, + private val altNames: List? = emptyList(), + private val englishName: String? = null, ) { fun toSManga() = SManga.create().apply { title = englishName ?: name @@ -108,15 +103,15 @@ data class Manga( // chapters details @Serializable -data class ChapterListData( +class ChapterListData( @SerialName("episodeInfos") val chapterList: List? = emptyList(), ) @Serializable -data class ChapterData( +class ChapterData( @SerialName("episodeIdNum") val chapterNum: JsonPrimitive, @SerialName("notes") val title: String? = null, - val uploadDates: DateDto? = null, + private val uploadDates: DateDto? = null, ) { fun toSChapter(mangaUrl: String) = SChapter.create().apply { name = "Chapter $chapterNum" @@ -133,23 +128,23 @@ data class ChapterData( } @Serializable -data class DateDto( +class DateDto( val sub: String? = null, ) -// page lsit +// page list @Serializable -data class PageListData( +class PageListData( @SerialName("chapterPages") val pageList: Edges?, ) @Serializable -data class Servers( +class Servers( @SerialName("pictureUrlHead") val serverUrl: String? = null, val pictureUrls: List?, ) @Serializable -data class PageUrl( +class PageUrl( val url: String, ) diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeFiters.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Filters.kt similarity index 100% rename from src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeFiters.kt rename to src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Filters.kt diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/PayloadDto.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/PayloadDto.kt new file mode 100644 index 000000000..108fc88e0 --- /dev/null +++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/PayloadDto.kt @@ -0,0 +1,58 @@ +package eu.kanade.tachiyomi.extension.en.allanime + +import kotlinx.serialization.Serializable + +@Serializable +class GraphQL( + private val variables: T, + private val query: String, +) + +@Serializable +class PopularVariables( + private val type: String, + private val size: Int, + private val dateRange: Int, + private val page: Int, + private val allowAdult: Boolean, + private val allowUnknown: Boolean, +) + +@Serializable +class SearchVariables( + private val search: SearchPayload, + private val size: Int, + private val page: Int, + private val translationType: String, + private val countryOrigin: String, +) + +@Serializable +class SearchPayload( + private val query: String?, + private val sortBy: String?, + private val genres: List?, + private val excludeGenres: List?, + private val isManga: Boolean, + private val allowAdult: Boolean, + private val allowUnknown: Boolean, +) + +@Serializable +class IDVariables( + private val id: String, +) + +@Serializable +class ChapterListVariables( + private val id: String, + private val chapterNumStart: Float, + private val chapterNumEnd: Float, +) + +@Serializable +class PageListVariables( + private val id: String, + private val chapterNum: String, + private val translationType: String, +) diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeQueries.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Queries.kt similarity index 98% rename from src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeQueries.kt rename to src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Queries.kt index 2128947b7..3a15330db 100644 --- a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/AllAnimeQueries.kt +++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Queries.kt @@ -41,14 +41,14 @@ val SEARCH_QUERY: String = buildQuery { """ query ( %search: SearchInput - %limit: Int + %size: Int %page: Int %translationType: VaildTranslationTypeMangaEnumType %countryOrigin: VaildCountryOriginEnumType ) { mangas( search: %search - limit: %limit + limit: %size page: %page translationType: %translationType countryOrigin: %countryOrigin diff --git a/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Utils.kt b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Utils.kt new file mode 100644 index 000000000..05c2c1df2 --- /dev/null +++ b/src/en/allanime/src/eu/kanade/tachiyomi/extension/en/allanime/Utils.kt @@ -0,0 +1,72 @@ +package eu.kanade.tachiyomi.extension.en.allanime + +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.jsoup.Jsoup +import java.text.SimpleDateFormat +import java.util.Locale + +val json: Json = Json { + ignoreUnknownKeys = true + explicitNulls = false + encodeDefaults = true + coerceInputValues = true +} + +fun String.parseThumbnailUrl(): String { + return if (this.matches(AllManga.urlRegex)) { + this + } else { + "$thumbnail_cdn$this?w=250" + } +} + +fun String?.parseStatus(): Int { + if (this == null) { + return SManga.UNKNOWN + } + + return when { + this.contains("releasing", true) -> SManga.ONGOING + this.contains("finished", true) -> SManga.COMPLETED + else -> SManga.UNKNOWN + } +} + +fun String.titleToSlug() = this.trim() + .lowercase(Locale.US) + .replace(titleSpecialCharactersRegex, "-") + +fun String.parseDescription(): String { + return Jsoup.parse( + this.replace("
", "br2n"), + ).text().replace("br2n", "\n") +} + +fun String?.parseDate(): Long { + return runCatching { + dateFormat.parse(this!!)!!.time + }.getOrDefault(0L) +} + +inline fun Response.parseAs(): T = json.decodeFromString(body.string()) + +inline fun List<*>.firstInstanceOrNull(): T? = + filterIsInstance().firstOrNull() + +inline fun T.toJsonRequestBody(): RequestBody = + json.encodeToString(this) + .toRequestBody(JSON_MEDIA_TYPE) + +private const val thumbnail_cdn = "https://wp.youtube-anime.com/aln.youtube-anime.com/" +private val titleSpecialCharactersRegex = Regex("[^a-z\\d]+") +private val dateFormat by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) +} +val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()